Initial EGit contribution to eclipse.org
[egit.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / project / GitProjectData.java
blob32ac2f0fe106838d21eedcd4690199ee436fd7c1
1 /*******************************************************************************
2 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
3 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * Copyright (C) 2008, Google Inc.
6 * All rights reserved. This program and the accompanying materials
7 * are made available under the terms of the Eclipse Public License v1.0
8 * which accompanies this distribution, and is available at
9 * http://www.eclipse.org/legal/epl-v10.html
10 *******************************************************************************/
11 package org.eclipse.egit.core.project;
13 import java.io.File;
14 import java.io.FileInputStream;
15 import java.io.FileNotFoundException;
16 import java.io.FileOutputStream;
17 import java.io.IOException;
18 import java.lang.ref.Reference;
19 import java.lang.ref.WeakReference;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.Iterator;
25 import java.util.Map;
26 import java.util.Properties;
27 import java.util.Set;
29 import org.eclipse.core.resources.IContainer;
30 import org.eclipse.core.resources.IProject;
31 import org.eclipse.core.resources.IResource;
32 import org.eclipse.core.resources.IResourceChangeEvent;
33 import org.eclipse.core.resources.IResourceChangeListener;
34 import org.eclipse.core.resources.ResourcesPlugin;
35 import org.eclipse.core.runtime.CoreException;
36 import org.eclipse.core.runtime.Preferences;
37 import org.eclipse.core.runtime.QualifiedName;
38 import org.eclipse.egit.core.Activator;
39 import org.eclipse.egit.core.CoreText;
40 import org.eclipse.egit.core.GitCorePreferences;
41 import org.eclipse.egit.core.GitProvider;
42 import org.eclipse.osgi.util.NLS;
43 import org.eclipse.team.core.RepositoryProvider;
44 import org.eclipse.jgit.lib.Repository;
45 import org.eclipse.jgit.lib.WindowCache;
46 import org.eclipse.jgit.lib.WindowCacheConfig;
48 /**
49 * This class keeps information about how a project is mapped to
50 * a Git repository.
52 public class GitProjectData {
53 private static final Map<IProject, GitProjectData> projectDataCache = new HashMap<IProject, GitProjectData>();
55 private static final Map<File, WeakReference> repositoryCache = new HashMap<File, WeakReference>();
57 private static Set<RepositoryChangeListener> repositoryChangeListeners = new HashSet<RepositoryChangeListener>();
59 @SuppressWarnings("synthetic-access")
60 private static final IResourceChangeListener rcl = new RCL();
62 private static class RCL implements IResourceChangeListener {
63 @SuppressWarnings("synthetic-access")
64 public void resourceChanged(final IResourceChangeEvent event) {
65 switch (event.getType()) {
66 case IResourceChangeEvent.PRE_CLOSE:
67 uncache((IProject) event.getResource());
68 break;
69 case IResourceChangeEvent.PRE_DELETE:
70 delete((IProject) event.getResource());
71 break;
72 default:
73 break;
78 private static QualifiedName MAPPING_KEY = new QualifiedName(
79 GitProjectData.class.getName(), "RepositoryMapping");
81 /**
82 * Start listening for resource changes.
84 * @param includeChange true to listen to content changes
86 public static void attachToWorkspace(final boolean includeChange) {
87 trace("attachToWorkspace - addResourceChangeListener");
88 ResourcesPlugin.getWorkspace().addResourceChangeListener(
89 rcl,
90 (includeChange ? IResourceChangeEvent.POST_CHANGE : 0)
91 | IResourceChangeEvent.PRE_CLOSE
92 | IResourceChangeEvent.PRE_DELETE);
95 /**
96 * Stop listening to resource changes
98 public static void detachFromWorkspace() {
99 trace("detachFromWorkspace - removeResourceChangeListener");
100 ResourcesPlugin.getWorkspace().removeResourceChangeListener(rcl);
104 * Register a new listener for repository modification events.
105 * <p>
106 * This is a no-op if <code>objectThatCares</code> has already been
107 * registered.
108 * </p>
110 * @param objectThatCares
111 * the new listener to register. Must not be null.
113 public static synchronized void addRepositoryChangeListener(
114 final RepositoryChangeListener objectThatCares) {
115 if (objectThatCares == null)
116 throw new NullPointerException();
117 repositoryChangeListeners.add(objectThatCares);
121 * Remove a registered {@link RepositoryChangeListener}
123 * @param objectThatCares
124 * The listener to remove
126 public static synchronized void removeRepositoryChangeListener(
127 final RepositoryChangeListener objectThatCares) {
128 repositoryChangeListeners.remove(objectThatCares);
132 * Notify registered {@link RepositoryChangeListener}s of a change.
134 * @param which
135 * the repository which has had changes occur within it.
137 static void fireRepositoryChanged(final RepositoryMapping which) {
138 for (RepositoryChangeListener listener : getRepositoryChangeListeners())
139 listener.repositoryChanged(which);
143 * Get a copy of the current set of repository change listeners
144 * <p>
145 * The array has no references, so is safe for iteration and modification
147 * @return a copy of the current repository change listeners
149 private static synchronized RepositoryChangeListener[] getRepositoryChangeListeners() {
150 return repositoryChangeListeners
151 .toArray(new RepositoryChangeListener[repositoryChangeListeners
152 .size()]);
156 * @param p
157 * @return {@link GitProjectData} for the specified project
159 public synchronized static GitProjectData get(final IProject p) {
160 try {
161 GitProjectData d = lookup(p);
162 if (d == null
163 && RepositoryProvider.getProvider(p) instanceof GitProvider) {
164 d = new GitProjectData(p).load();
165 cache(p, d);
167 return d;
168 } catch (IOException err) {
169 Activator.logError(CoreText.GitProjectData_missing, err);
170 return null;
175 * Drop the Eclipse project from our association of projects/repositories
177 * @param p Eclipse project
179 public static void delete(final IProject p) {
180 trace("delete(" + p.getName() + ")");
181 GitProjectData d = lookup(p);
182 if (d == null) {
183 try {
184 d = new GitProjectData(p).load();
185 } catch (IOException ioe) {
186 d = new GitProjectData(p);
189 d.delete();
192 static void trace(final String m) {
193 Activator.trace("(GitProjectData) " + m);
196 private synchronized static void cache(final IProject p,
197 final GitProjectData d) {
198 projectDataCache.put(p, d);
201 private synchronized static void uncache(final IProject p) {
202 if (projectDataCache.remove(p) != null) {
203 trace("uncacheDataFor(" + p.getName() + ")");
207 private synchronized static GitProjectData lookup(final IProject p) {
208 return projectDataCache.get(p);
211 private synchronized static Repository lookupRepository(final File gitDir)
212 throws IOException {
213 final Iterator i = repositoryCache.entrySet().iterator();
214 while (i.hasNext()) {
215 final Map.Entry e = (Map.Entry) i.next();
216 if (((Reference) e.getValue()).get() == null) {
217 i.remove();
221 final Reference r = repositoryCache.get(gitDir);
222 Repository d = r != null ? (Repository) r.get() : null;
223 if (d == null) {
224 d = new Repository(gitDir);
225 repositoryCache.put(gitDir, new WeakReference<Repository>(d));
227 return d;
231 * Update the settings for the global window cache of the workspace.
233 public static void reconfigureWindowCache() {
234 final WindowCacheConfig c = new WindowCacheConfig();
235 Preferences p = Activator.getDefault().getPluginPreferences();
236 c.setPackedGitLimit(p.getInt(GitCorePreferences.core_packedGitLimit));
237 c.setPackedGitWindowSize(p.getInt(GitCorePreferences.core_packedGitWindowSize));
238 c.setPackedGitMMAP(p.getBoolean(GitCorePreferences.core_packedGitMMAP));
239 c.setDeltaBaseCacheLimit(p.getInt(GitCorePreferences.core_deltaBaseCacheLimit));
240 WindowCache.reconfigure(c);
243 private final IProject project;
245 private final Collection<RepositoryMapping> mappings = new ArrayList<RepositoryMapping>();
247 private final Set<IResource> protectedResources = new HashSet<IResource>();
250 * Construct a {@link GitProjectData} for the mapping
251 * of a project.
253 * @param p Eclipse project
255 public GitProjectData(final IProject p) {
256 project = p;
260 * @return the Eclipse project mapped through this resource.
262 public IProject getProject() {
263 return project;
267 * TODO: is this right?
269 * @param newMappings
271 public void setRepositoryMappings(final Collection<RepositoryMapping> newMappings) {
272 mappings.clear();
273 mappings.addAll(newMappings);
274 remapAll();
278 * Hide our private parts from the navigators other browsers.
280 * @throws CoreException
282 public void markTeamPrivateResources() throws CoreException {
283 for (final Object rmObj : mappings) {
284 final RepositoryMapping rm = (RepositoryMapping)rmObj;
285 final IContainer c = rm.getContainer();
286 if (c == null)
287 continue; // Not fully mapped yet?
289 final IResource dotGit = c.findMember(".git");
290 if (dotGit != null) {
291 try {
292 final Repository r = rm.getRepository();
293 final File dotGitDir = dotGit.getLocation().toFile()
294 .getCanonicalFile();
295 if (dotGitDir.equals(r.getDirectory())) {
296 trace("teamPrivate " + dotGit);
297 dotGit.setTeamPrivateMember(true);
299 } catch (IOException err) {
300 throw Activator.error(CoreText.Error_CanonicalFile, err);
307 * @param f
308 * @return true if a resource is protected in this repository
310 public boolean isProtected(final IResource f) {
311 return protectedResources.contains(f);
315 * @param r any workbench resource contained within this project.
316 * @return the mapping for the specified project
318 public RepositoryMapping getRepositoryMapping(IResource r) {
319 try {
320 for (; r != null; r = r.getParent()) {
321 final RepositoryMapping m;
323 if (!r.isAccessible())
324 continue;
325 m = (RepositoryMapping) r.getSessionProperty(MAPPING_KEY);
326 if (m != null)
327 return m;
329 } catch (CoreException err) {
330 Activator.logError("Failed finding RepositoryMapping", err);
332 return null;
335 private void delete() {
336 final File dir = propertyFile().getParentFile();
337 final File[] todel = dir.listFiles();
338 if (todel != null) {
339 for (int k = 0; k < todel.length; k++) {
340 if (todel[k].isFile()) {
341 todel[k].delete();
345 dir.delete();
346 trace("deleteDataFor(" + getProject().getName() + ")");
347 uncache(getProject());
351 * Store information about the repository connection in the workspace
353 * @throws CoreException
355 public void store() throws CoreException {
356 final File dat = propertyFile();
357 final File tmp;
358 boolean ok = false;
360 try {
361 trace("save " + dat);
362 tmp = File.createTempFile("gpd_", ".prop", dat.getParentFile());
363 final FileOutputStream o = new FileOutputStream(tmp);
364 try {
365 final Properties p = new Properties();
366 final Iterator i = mappings.iterator();
367 while (i.hasNext()) {
368 ((RepositoryMapping) i.next()).store(p);
370 p.store(o, "GitProjectData");
371 ok = true;
372 } finally {
373 o.close();
374 if (!ok) {
375 tmp.delete();
378 } catch (IOException ioe) {
379 throw Activator.error(NLS.bind(CoreText.GitProjectData_saveFailed,
380 dat), ioe);
383 dat.delete();
384 if (!tmp.renameTo(dat)) {
385 tmp.delete();
386 throw Activator.error(NLS.bind(CoreText.GitProjectData_saveFailed,
387 dat), null);
391 private File propertyFile() {
392 return new File(getProject()
393 .getWorkingLocation(Activator.getPluginId()).toFile(),
394 "GitProjectData.properties");
397 private GitProjectData load() throws IOException {
398 final File dat = propertyFile();
399 trace("load " + dat);
401 final FileInputStream o = new FileInputStream(dat);
402 try {
403 final Properties p = new Properties();
404 p.load(o);
406 mappings.clear();
407 final Iterator keyItr = p.keySet().iterator();
408 while (keyItr.hasNext()) {
409 final String key = keyItr.next().toString();
410 if (RepositoryMapping.isInitialKey(key)) {
411 mappings.add(new RepositoryMapping(p, key));
414 } finally {
415 o.close();
418 remapAll();
419 return this;
422 private void remapAll() {
423 protectedResources.clear();
424 final Iterator i = mappings.iterator();
425 while (i.hasNext()) {
426 map((RepositoryMapping) i.next());
430 private void map(final RepositoryMapping m) {
431 final IResource r;
432 final File git;
433 final IResource dotGit;
434 IContainer c = null;
436 m.clear();
437 r = getProject().findMember(m.getContainerPath());
438 if (r instanceof IContainer) {
439 c = (IContainer) r;
440 } else {
441 c = (IContainer) r.getAdapter(IContainer.class);
444 if (c == null) {
445 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
446 new FileNotFoundException(m.getContainerPath().toString()));
447 m.clear();
448 return;
450 m.setContainer(c);
452 git = c.getLocation().append(m.getGitDirPath()).toFile();
453 if (!git.isDirectory() || !new File(git, "config").isFile()) {
454 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
455 new FileNotFoundException(m.getContainerPath().toString()));
456 m.clear();
457 return;
460 try {
461 m.setRepository(lookupRepository(git));
462 } catch (IOException ioe) {
463 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
464 new FileNotFoundException(m.getContainerPath().toString()));
465 m.clear();
466 return;
469 m.fireRepositoryChanged();
471 trace("map " + c + " -> " + m.getRepository());
472 try {
473 c.setSessionProperty(MAPPING_KEY, m);
474 } catch (CoreException err) {
475 Activator.logError("Failed to cache RepositoryMapping", err);
478 dotGit = c.findMember(".git");
479 if (dotGit != null && dotGit.getLocation().toFile().equals(git)) {
480 protect(dotGit);
484 private void protect(IResource c) {
485 while (c != null && !c.equals(getProject())) {
486 trace("protect " + c);
487 protectedResources.add(c);
488 c = c.getParent();