Detect resource changes.
[egit.git] / org.spearce.egit.core / src / org / spearce / egit / core / project / GitProjectData.java
blob431e0c4ccaf75984ceccffa0078d56d0a107173b
1 /*
2 * Copyright (C) 2006 Shawn Pearce <spearce@spearce.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License, version 2.1, as published by the Free Software Foundation.
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * Lesser General Public License for more details.
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
17 package org.spearce.egit.core.project;
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileNotFoundException;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.lang.ref.Reference;
25 import java.lang.ref.WeakReference;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.Iterator;
31 import java.util.Map;
32 import java.util.Properties;
33 import java.util.Set;
35 import org.eclipse.core.resources.IContainer;
36 import org.eclipse.core.resources.IProject;
37 import org.eclipse.core.resources.IResource;
38 import org.eclipse.core.resources.IResourceChangeEvent;
39 import org.eclipse.core.resources.IResourceChangeListener;
40 import org.eclipse.core.resources.IResourceDelta;
41 import org.eclipse.core.resources.IResourceDeltaVisitor;
42 import org.eclipse.core.resources.ResourcesPlugin;
43 import org.eclipse.core.runtime.CoreException;
44 import org.eclipse.osgi.util.NLS;
45 import org.eclipse.team.core.RepositoryProvider;
46 import org.spearce.egit.core.Activator;
47 import org.spearce.egit.core.CoreText;
48 import org.spearce.egit.core.GitProvider;
49 import org.spearce.jgit.lib.FileTreeEntry;
50 import org.spearce.jgit.lib.Repository;
51 import org.spearce.jgit.lib.Tree;
52 import org.spearce.jgit.lib.TreeEntry;
54 public class GitProjectData {
55 private static final Map projectDataCache = new HashMap();
57 private static final Map repositoryCache = new HashMap();
59 private static RepositoryChangeListener[] repositoryChangeListeners = {};
61 private static final IResourceChangeListener rcl = new RCL();
63 private static class RCL implements IResourceChangeListener {
64 public void resourceChanged(final IResourceChangeEvent event) {
65 switch (event.getType()) {
66 case IResourceChangeEvent.POST_CHANGE:
67 projectsChanged(event.getDelta().getAffectedChildren(
68 IResourceDelta.CHANGED));
69 break;
70 case IResourceChangeEvent.PRE_CLOSE:
71 uncache((IProject) event.getResource());
72 break;
73 case IResourceChangeEvent.PRE_DELETE:
74 delete((IProject) event.getResource());
75 break;
76 default:
77 break;
82 public static void attachToWorkspace(final boolean includeChange) {
83 trace("attachToWorkspace - addResourceChangeListener");
84 ResourcesPlugin.getWorkspace().addResourceChangeListener(
85 rcl,
86 (includeChange ? IResourceChangeEvent.POST_CHANGE : 0)
87 | IResourceChangeEvent.PRE_CLOSE
88 | IResourceChangeEvent.PRE_DELETE);
91 public static void detachFromWorkspace() {
92 trace("detachFromWorkspace - removeResourceChangeListener");
93 ResourcesPlugin.getWorkspace().removeResourceChangeListener(rcl);
96 /**
97 * Register a new listener for repository modification events.
98 * <p>
99 * This is a no-op if <code>objectThatCares</code> has already been
100 * registered.
101 * </p>
103 * @param objectThatCares
104 * the new listener to register. Must not be null.
106 public static synchronized void addRepositoryChangeListener(
107 final RepositoryChangeListener objectThatCares) {
108 if (objectThatCares == null)
109 throw new NullPointerException();
110 for (int k = repositoryChangeListeners.length - 1; k >= 0; k--) {
111 if (repositoryChangeListeners[k] == objectThatCares)
112 return;
114 final int p = repositoryChangeListeners.length;
115 final RepositoryChangeListener[] n;
116 n = new RepositoryChangeListener[p + 1];
117 System.arraycopy(repositoryChangeListeners, 0, n, 0, p);
118 n[p] = objectThatCares;
119 repositoryChangeListeners = n;
123 * Notify registered {@link RepositoryChangeListener}s of a change.
125 * @param which
126 * the repository which has had changes occur within it.
128 static void fireRepositoryChanged(final RepositoryMapping which) {
129 final RepositoryChangeListener[] e = getRepositoryChangeListeners();
130 for (int k = e.length - 1; k >= 0; k--)
131 e[k].repositoryChanged(which);
134 private static synchronized RepositoryChangeListener[] getRepositoryChangeListeners() {
135 return repositoryChangeListeners;
138 public synchronized static GitProjectData get(final IProject p) {
139 try {
140 GitProjectData d = lookup(p);
141 if (d == null
142 && RepositoryProvider.getProvider(p) instanceof GitProvider) {
143 d = new GitProjectData(p).load();
144 cache(p, d);
146 return d;
147 } catch (IOException err) {
148 Activator.logError(CoreText.GitProjectData_missing, err);
149 return null;
153 public static void delete(final IProject p) {
154 trace("delete(" + p.getName() + ")");
155 GitProjectData d = lookup(p);
156 if (d == null) {
157 try {
158 d = new GitProjectData(p).load();
159 } catch (IOException ioe) {
160 d = new GitProjectData(p);
163 d.delete();
166 public static synchronized void checkpointAllProjects() {
167 final Iterator i = projectDataCache.values().iterator();
168 while (i.hasNext()) {
169 ((GitProjectData) i.next()).checkpointIfNecessary();
173 private static void trace(final String m) {
174 Activator.trace("(GitProjectData) " + m);
177 private static void projectsChanged(final IResourceDelta[] projDeltas) {
178 for (int k = 0; k < projDeltas.length; k++) {
179 final IResource r = projDeltas[k].getResource();
180 final GitProjectData d = get((IProject) r);
181 if (d != null) {
182 d.notifyChanged(projDeltas[k]);
187 private synchronized static void cache(final IProject p,
188 final GitProjectData d) {
189 projectDataCache.put(p, d);
192 private synchronized static void uncache(final IProject p) {
193 if (projectDataCache.remove(p) != null) {
194 trace("uncacheDataFor(" + p.getName() + ")");
198 private synchronized static GitProjectData lookup(final IProject p) {
199 return (GitProjectData) projectDataCache.get(p);
202 private synchronized static Repository lookupRepository(final File gitDir)
203 throws IOException {
204 final Iterator i = repositoryCache.entrySet().iterator();
205 while (i.hasNext()) {
206 final Map.Entry e = (Map.Entry) i.next();
207 if (((Reference) e.getValue()).get() == null) {
208 i.remove();
212 final Reference r = (Reference) repositoryCache.get(gitDir);
213 Repository d = r != null ? (Repository) r.get() : null;
214 if (d == null) {
215 d = new Repository(gitDir);
216 repositoryCache.put(gitDir, new WeakReference(d));
218 return d;
221 private final IProject project;
223 private final Collection mappings;
225 private final Map c2mapping;
227 private final Set protectedResources;
229 public GitProjectData(final IProject p) {
230 project = p;
231 mappings = new ArrayList();
232 c2mapping = new HashMap();
233 protectedResources = new HashSet();
236 public IProject getProject() {
237 return project;
240 public void setRepositoryMappings(final Collection newMappings) {
241 mappings.clear();
242 mappings.addAll(newMappings);
243 remapAll();
246 public void markTeamPrivateResources() throws CoreException {
247 final Iterator i = c2mapping.entrySet().iterator();
248 while (i.hasNext()) {
249 final Map.Entry e = (Map.Entry) i.next();
250 final IContainer c = (IContainer) e.getKey();
251 final IResource dotGit = c.findMember(".git");
252 if (dotGit != null) {
253 try {
254 final Repository r = ((RepositoryMapping) e.getValue())
255 .getRepository();
256 final File dotGitDir = dotGit.getLocation().toFile()
257 .getCanonicalFile();
258 if (dotGitDir.equals(r.getDirectory())) {
259 trace("teamPrivate " + dotGit);
260 dotGit.setTeamPrivateMember(true);
262 } catch (IOException err) {
263 throw Activator.error(CoreText.Error_CanonicalFile, err);
269 public boolean isProtected(final IResource f) {
270 return protectedResources.contains(f);
273 public RepositoryMapping getRepositoryMapping(final IResource r) {
274 return (RepositoryMapping) c2mapping.get(r);
277 public TreeEntry[] getActiveDiffTreeEntries(IResource res)
278 throws CoreException {
279 String s = null;
280 RepositoryMapping m = null;
282 IResource r = res;
283 while (r != null) {
284 m = getRepositoryMapping(r);
285 if (m != null) {
286 break;
289 if (s != null) {
290 s = r.getName() + "/" + s;
291 } else {
292 s = r.getName();
295 r = r.getParent();
298 if (s != null && m != null && m.getActiveDiff() != null) {
299 try {
300 if (res.getType() == IResource.FILE)
301 return m.getActiveDiff().findBlobMember(s);
302 else
303 return m.getActiveDiff().findTreeMember(s);
304 } catch (IOException ioe) {
305 throw Activator.error(
306 CoreText.GitProjectData_lazyResolveFailed, ioe);
310 return null;
313 public void checkpointIfNecessary() {
314 final Iterator i = c2mapping.values().iterator();
315 while (i.hasNext()) {
316 ((RepositoryMapping) i.next()).checkpointIfNecessary();
320 public void fullUpdate() {
321 final Iterator i = c2mapping.values().iterator();
322 while (i.hasNext()) {
323 try {
324 ((RepositoryMapping) i.next()).fullUpdate();
325 } catch (IOException ioe) {
326 Activator.logError(CoreText.GitProjectData_cannotReadHEAD, ioe);
327 return;
332 public void delete() {
333 final File dir = propertyFile().getParentFile();
334 final File[] todel = dir.listFiles();
335 if (todel != null) {
336 for (int k = 0; k < todel.length; k++) {
337 if (todel[k].isFile()) {
338 todel[k].delete();
342 dir.delete();
343 trace("deleteDataFor(" + getProject().getName() + ")");
344 uncache(getProject());
347 public void store() throws CoreException {
348 final File dat = propertyFile();
349 final File tmp;
350 boolean ok = false;
352 try {
353 trace("save " + dat);
354 tmp = File.createTempFile("gpd_", ".prop", dat.getParentFile());
355 final FileOutputStream o = new FileOutputStream(tmp);
356 try {
357 final Properties p = new Properties();
358 final Iterator i = mappings.iterator();
359 while (i.hasNext()) {
360 ((RepositoryMapping) i.next()).store(p);
362 p.store(o, "GitProjectData");
363 ok = true;
364 } finally {
365 o.close();
366 if (!ok) {
367 tmp.delete();
370 } catch (IOException ioe) {
371 throw Activator.error(NLS.bind(CoreText.GitProjectData_saveFailed,
372 dat), ioe);
375 dat.delete();
376 if (!tmp.renameTo(dat)) {
377 tmp.delete();
378 throw Activator.error(NLS.bind(CoreText.GitProjectData_saveFailed,
379 dat), null);
383 private void notifyChanged(final IResourceDelta projDelta) {
384 final Set affectedMappings = new HashSet();
385 try {
386 projDelta.accept(new IResourceDeltaVisitor() {
387 public boolean visit(final IResourceDelta d)
388 throws CoreException {
389 final int f = d.getFlags();
390 IResource res = d.getResource();
391 IResource r = res;
392 if ((f & IResourceDelta.CONTENT) != 0
393 || (f & IResourceDelta.ENCODING) != 0
394 || r instanceof IContainer) {
395 String s = null;
396 RepositoryMapping m = null;
398 while (r != null) {
399 m = getRepositoryMapping(r);
400 if (m != null) {
401 break;
404 if (s != null) {
405 s = r.getName() + "/" + s;
406 } else {
407 s = r.getName();
410 r = r.getParent();
413 if (m == null) {
414 return false;
415 } else if (s == null) {
416 return true;
419 final Tree cacheTree = m.getCacheTree();
420 if (cacheTree != null) {
421 try {
422 synchronized (cacheTree) {
423 final TreeEntry e;
424 if (res.getType() == IResource.FILE)
425 e = cacheTree.findBlobMember(s);
426 else
427 e = cacheTree.findTreeMember(s);
428 if (e instanceof FileTreeEntry) {
429 trace("modified " + r + " -> "
430 + e.getFullName());
431 e.setModified();
432 affectedMappings.add(m);
435 } catch (IOException ioe) {
436 throw Activator
437 .error(
438 CoreText.GitProjectData_lazyResolveFailed,
439 ioe);
441 return true;
444 return false;
447 } catch (CoreException ce) {
448 // We are in deep trouble. This should NOT have happend. Detach
449 // our listeners and forget it ever did.
451 attachToWorkspace(false);
452 Activator.logError(CoreText.GitProjectData_notifyChangedFailed, ce);
455 try {
456 final Iterator i = affectedMappings.iterator();
457 while (i.hasNext()) {
458 ((RepositoryMapping) i.next()).recomputeMerge();
460 } catch (IOException ioe) {
461 Activator
462 .logError(CoreText.GitProjectData_notifyChangedFailed, ioe);
466 private File propertyFile() {
467 return new File(getProject()
468 .getWorkingLocation(Activator.getPluginId()).toFile(),
469 "GitProjectData.properties");
472 private GitProjectData load() throws IOException {
473 final File dat = propertyFile();
474 trace("load " + dat);
476 final FileInputStream o = new FileInputStream(dat);
477 try {
478 final Properties p = new Properties();
479 p.load(o);
481 mappings.clear();
482 final Iterator keyItr = p.keySet().iterator();
483 while (keyItr.hasNext()) {
484 final String key = keyItr.next().toString();
485 if (RepositoryMapping.isInitialKey(key)) {
486 mappings.add(new RepositoryMapping(p, key));
489 } finally {
490 o.close();
493 remapAll();
494 return this;
497 private void remapAll() {
498 protectedResources.clear();
499 final Iterator i = mappings.iterator();
500 while (i.hasNext()) {
501 map((RepositoryMapping) i.next());
505 private void map(final RepositoryMapping m) {
506 final IResource r;
507 final File git;
508 final IResource dotGit;
509 IContainer c = null;
511 m.clear();
512 r = getProject().findMember(m.getContainerPath());
513 if (r instanceof IContainer) {
514 c = (IContainer) r;
515 } else {
516 c = (IContainer) r.getAdapter(IContainer.class);
519 if (c == null) {
520 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
521 new FileNotFoundException(m.getContainerPath().toString()));
522 m.clear();
523 return;
525 m.setContainer(c);
527 git = c.getLocation().append(m.getGitDirPath()).toFile();
528 if (!git.isDirectory() || !new File(git, "config").isFile()) {
529 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
530 new FileNotFoundException(m.getContainerPath().toString()));
531 m.clear();
532 return;
535 try {
536 m.setRepository(lookupRepository(git));
537 } catch (IOException ioe) {
538 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
539 new FileNotFoundException(m.getContainerPath().toString()));
540 m.clear();
541 return;
544 try {
545 m.recomputeMerge();
546 } catch (IOException ioe) {
547 Activator.logError(CoreText.GitProjectData_cannotReadHEAD, ioe);
548 m.clear();
549 return;
552 trace("map " + c + " -> " + m.getRepository());
553 c2mapping.put(c, m);
555 dotGit = c.findMember(".git");
556 if (dotGit != null && dotGit.getLocation().toFile().equals(git)) {
557 protect(dotGit);
561 private void protect(IResource c) {
562 while (c != null && !c.equals(getProject())) {
563 trace("protect " + c);
564 protectedResources.add(c);
565 c = c.getParent();