Subject: [EGIT PATCH 11/11] Implement label decorations for folders and projects

From: Tor Arne Vestbø <[email-masked]>
Date: Thu, 5 Feb 2009 02:00:18 +0100

The option "Inspect dirty state of children..." controls
if the decoration process should look at child resources
to decide if a container is dirty or not. For large/deep
projects this can be quite time consuming.

The other option, "Also re-decorate ancestors..." controls if
parents of a re-decorated resource also should be updated, for
example to signal that the containing folder is now dirty.

Signed-off-by: Tor Arne Vestbø <[email-masked]>
---
 org.spearce.egit.ui/.options                       |    8 +-
 .../src/org/spearce/egit/ui/Activator.java         |    2 +-
 .../egit/ui/PluginPreferenceInitializer.java       |    3 +-
 .../src/org/spearce/egit/ui/UIPreferences.java     |    4 +-
 .../src/org/spearce/egit/ui/UIText.java            |    5 +-
 .../decorators/GitLightweightDecorator.java        |  344 +++++++++++++++-----
 .../preferences/GitDecoratorPreferencePage.java    |   33 ++-
 .../src/org/spearce/egit/ui/uitext.properties      |    3 +-
 8 files changed, 311 insertions(+), 91 deletions(-)

diff --git a/org.spearce.egit.ui/.options b/org.spearce.egit.ui/.options
index a084b35..8fc1c19 100644
--- a/org.spearce.egit.ui/.options
+++ b/org.spearce.egit.ui/.options
@@ -1 +1,7 @@
-org.spearce.egit.ui/trace/verbose = false
+# Debugging options for the org.spearce.egit.ui plugin.
+
+# Show general verbose output
+org.spearce.egit.ui/verbose = false
+
+# Show debug output for label decorations
+org.spearce.egit.ui/decorations = false
\ No newline at end of file
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java
index 9d03c70..45010ce 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java
@@ -190,7 +190,7 @@ public Activator() {
 
 	public void start(final BundleContext context) throws Exception {
 		super.start(context);
-		traceVerbose = isOptionSet("/trace/verbose");
+		traceVerbose = isOptionSet("/verbose");
 		setupSSH(context);
 		setupProxy(context);
 		setupRepoChangeScanner();
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java
index ef886cf..9af35bc 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java
@@ -35,7 +35,8 @@ public void initializeDefaultPreferences() {
 		prefs.setDefault(UIPreferences.RESOURCEHISTORY_SHOW_REV_COMMENT, true);
 		prefs.setDefault(UIPreferences.RESOURCEHISTORY_SHOW_TOOLTIPS, false);
 
-		prefs.setDefault(UIPreferences.DECORATOR_CALCULATE_DIRTY, true);
+		prefs.setDefault(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS, true);
+		prefs.setDefault(UIPreferences.DECORATOR_COMPUTE_DEEP_DIRTY, true);
 		prefs.setDefault(UIPreferences.DECORATOR_FILETEXT_DECORATION,
 				UIText.DecoratorPreferencesPage_fileFormatDefault);
 		prefs.setDefault(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION,
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java
index 409b335..f0a5e28 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java
@@ -53,7 +53,9 @@
 	public final static String THEME_CommitMessageFont = "org.spearce.egit.ui.CommitMessageFont";
 
 	/** */
-	public final static String DECORATOR_CALCULATE_DIRTY = "decorator_calculate_dirty";
+	public final static String DECORATOR_RECOMPUTE_ANCESTORS = "decorator_recompute_ancestors";
+	/** */
+	public final static String DECORATOR_COMPUTE_DEEP_DIRTY = "decorator_compute_deep_dirty";
 	/** */
 	public final static String DECORATOR_FILETEXT_DECORATION = "decorator_filetext_decoration";
 	/** */
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java
index b62ca4c..5dd28df 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java
@@ -920,7 +920,10 @@
 	public static String DecoratorPreferencesPage_addVariablesAction;
 
 	/** */
-	public static String DecoratorPreferencesPage_computeDeep;
+	public static String DecoratorPreferencesPage_recomputeAncestorDecorations;
+
+	/** */
+	public static String DecoratorPreferencesPage_computeDeepDirtyState;
 
 	/** */
 	public static String DecoratorPreferencesPage_description;
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java
index 45b9f83..c72bfdb 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java
@@ -31,6 +31,7 @@
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
 import org.eclipse.jface.preference.IPreferenceStore;
 import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.jface.util.IPropertyChangeListener;
@@ -48,6 +49,7 @@
 import org.eclipse.team.ui.TeamUI;
 import org.eclipse.ui.IContributorResourceAdapter;
 import org.eclipse.ui.PlatformUI;
+import org.spearce.egit.core.ContainerTreeIterator;
 import org.spearce.egit.core.GitException;
 import org.spearce.egit.core.internal.util.ExceptionCollector;
 import org.spearce.egit.core.project.GitProjectData;
@@ -61,6 +63,8 @@
 import org.spearce.jgit.dircache.DirCache;
 import org.spearce.jgit.dircache.DirCacheEntry;
 import org.spearce.jgit.dircache.DirCacheIterator;
+import org.spearce.jgit.errors.IncorrectObjectTypeException;
+import org.spearce.jgit.errors.MissingObjectException;
 import org.spearce.jgit.lib.Constants;
 import org.spearce.jgit.lib.FileMode;
 import org.spearce.jgit.lib.IndexChangedEvent;
@@ -72,7 +76,11 @@
 import org.spearce.jgit.revwalk.RevWalk;
 import org.spearce.jgit.treewalk.EmptyTreeIterator;
 import org.spearce.jgit.treewalk.TreeWalk;
+import org.spearce.jgit.treewalk.WorkingTreeIterator;
+import org.spearce.jgit.treewalk.filter.AndTreeFilter;
 import org.spearce.jgit.treewalk.filter.PathFilterGroup;
+import org.spearce.jgit.treewalk.filter.TreeFilter;
+import org.spearce.jgit.util.FS;
 
 /**
  * Supplies annotations for displayed resources
@@ -110,6 +118,34 @@
 			IStatus.ERROR, Activator.getDefault().getLog());
 
 	/**
+	 * Property constant indicating if tracing/debugging of decorations is
+	 * enabled
+	 */
+	private static boolean DEBUG_DECORATIONS = false;
+
+	static {
+		DEBUG_DECORATIONS = "true".equals(Platform.getDebugOption(Activator.getPluginId() + "/decorations")); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	private static void debug(String what) {
+		StackTraceElement frame = Thread.currentThread().getStackTrace()[2];
+		if (frame.getMethodName().endsWith("access$1"))
+			frame = Thread.currentThread().getStackTrace()[3];
+		String className = frame.getClassName().substring(
+				frame.getClassName().lastIndexOf('.') + 1);
+		System.out.print(className + "::" + frame.getMethodName() + "(): ");
+
+		debug(what, 0);
+	}
+
+	private static void debug(String what, int numIndents) {
+		for (int i = 0; i < numIndents; ++i)
+			System.out.print("\t");
+
+		System.out.println(what);
+	}
+
+	/**
 	 * Constructs a new Git resource decorator
 	 */
 	public GitLightweightDecorator() {
@@ -174,6 +210,9 @@ public void decorate(Object element, IDecoration decoration) {
 		if (activator == null)
 			return;
 
+		if (DEBUG_DECORATIONS)
+			debug("deocrating '" + resource.getLocation() + "'");
+
 		try {
 			DecorationHelper helper = new DecorationHelper(activator
 					.getPreferenceStore());
@@ -184,7 +223,8 @@ public void decorate(Object element, IDecoration decoration) {
 		}
 	}
 
-	private class DecoratableResourceAdapter implements IDecoratableResource {
+	private static class DecoratableResourceAdapter implements
+			IDecoratableResource {
 
 		private final IResource resource;
 
@@ -212,6 +252,111 @@ public void decorate(Object element, IDecoration decoration) {
 
 		static final int T_WORKSPACE = 2;
 
+		private static class ContainerDiffFilter extends TreeFilter {
+			static final ContainerDiffFilter INSTANCE = new ContainerDiffFilter();
+
+			@Override
+			public boolean include(final TreeWalk tw)
+					throws MissingObjectException,
+					IncorrectObjectTypeException, IOException {
+
+				if (tw.getFileMode(T_HEAD) == FileMode.MISSING
+						&& tw.getFileMode(T_INDEX) == FileMode.MISSING)
+					return false; // Untracked and unstaged, so not dirty
+
+				if (DEBUG_DECORATIONS)
+					debug("checking '" + tw.getPathString() + "'");
+
+				int mHead = tw.getRawMode(T_HEAD);
+				int mIndex = tw.getRawMode(T_INDEX);
+				int mWorkspace = tw.getRawMode(T_WORKSPACE);
+
+				if (!FS.INSTANCE.supportsExecute()) {
+					// We need to clear the executable bits of the head and
+					// index modes, to prevent false positives when the
+					// resource is +x in the repository but not on disk
+					mHead &= (~0111);
+					mIndex &= (~0111);
+				}
+
+				if (DEBUG_DECORATIONS) {
+					debug("mHead=" + Integer.toOctalString(mHead) + " mIndex="
+							+ Integer.toOctalString(mIndex) + " mWorkspace="
+							+ Integer.toOctalString(mWorkspace), 1);
+				}
+
+				if (mHead != mIndex || mHead != mWorkspace) {
+					// If all three modes aren't identical there is a difference
+					// here. Its the fastest test we have and it neatly handles
+					// weird D->F/F->D style changes that may cause issues later
+
+					if (DEBUG_DECORATIONS) {
+						debug("Modes did not match, so concider the whole "
+								+ "tree dirty", 1);
+					}
+
+					return true;
+				}
+
+				if (FileMode.TREE.equals(mHead)) {
+					// We must include trees otherwise the walker won't recurse
+					// into a subtree for us. Computing anything more about a
+					// tree is too expensive from the workspace so we do not do
+					// a prune based on no changes.
+
+					final IPreferenceStore store = Activator.getDefault()
+							.getPreferenceStore();
+					return store
+							.getBoolean(UIPreferences.DECORATOR_COMPUTE_DEEP_DIRTY);
+				}
+
+				final DirCacheIterator iIndex = tw.getTree(T_INDEX,
+						DirCacheIterator.class);
+				final WorkingTreeIterator iWorkspace = tw.getTree(T_WORKSPACE,
+						WorkingTreeIterator.class);
+
+				if (!timestampMatches(iIndex, iWorkspace)) {
+					// If the modification time of the file differs we know the
+					// folder is (possibly) dirty and should be marked as such.
+					// This is faster than the id check below so it goes first.
+
+					if (DEBUG_DECORATIONS) {
+						debug("Timestamps did not match, so concider the "
+								+ "whole tree dirty", 1);
+					}
+
+					return true;
+				}
+
+				if (!tw.idEqual(T_HEAD, T_INDEX)) {
+					// Staged id difference indicates the path is modified.
+
+					if (DEBUG_DECORATIONS) {
+						debug("File is staged, so concider the whole "
+								+ "tree dirty", 1);
+					}
+
+					return true;
+				}
+
+				// TODO: Consider doing a content check here, to rule out false
+				// positives, as we might get mismatch between timestamps, even
+				// if the content is the same
+
+				return false;
+			}
+
+			@Override
+			public boolean shouldBeRecursive() {
+				return true;
+			}
+
+			@Override
+			public TreeFilter clone() {
+				return this;
+			}
+		}
+
 		public DecoratableResourceAdapter(IResource resourceToWrap)
 				throws IOException {
 			resource = resourceToWrap;
@@ -219,25 +364,12 @@ public DecoratableResourceAdapter(IResource resourceToWrap)
 			repository = mapping.getRepository();
 			headId = repository.resolve(Constants.HEAD);
 
-			switch (resource.getType()) {
-			case IResource.FILE:
-				extractFileProperties();
-				break;
-			case IResource.FOLDER:
-				extractContainerProperties();
-				break;
-			case IResource.PROJECT:
-				extractProjectProperties();
-				break;
-			}
-		}
+			// TODO: Add option to shorten branch name to 6 chars if it's a SHA
+			branch = repository.getBranch();
 
-		private void extractFileProperties() throws IOException {
 			TreeWalk treeWalk = createHeadVsIndexTreeWalk();
-			if (treeWalk == null)
-				return;
-
-			if (treeWalk.next())
+			if (resource.getType() == IResource.PROJECT
+					|| (treeWalk != null && treeWalk.next()))
 				tracked = true;
 			else
 				return;
@@ -248,6 +380,27 @@ private void extractFileProperties() throws IOException {
 				return;
 			}
 
+			switch (resource.getType()) {
+			case IResource.FILE:
+				extractFileProperties(treeWalk);
+				break;
+
+			case IResource.FOLDER:
+				extractContainerProperties();
+				break;
+
+			case IResource.PROJECT:
+				final IPreferenceStore store = Activator.getDefault()
+						.getPreferenceStore();
+				if (!store
+						.getBoolean(UIPreferences.DECORATOR_COMPUTE_DEEP_DIRTY))
+					return;
+				extractContainerProperties();
+				break;
+			}
+		}
+
+		private void extractFileProperties(TreeWalk treeWalk) {
 			final DirCacheIterator indexIterator = treeWalk.getTree(T_INDEX,
 					DirCacheIterator.class);
 			final DirCacheEntry indexEntry = indexIterator != null ? indexIterator
@@ -265,18 +418,7 @@ private void extractFileProperties() throws IOException {
 						.getBits()) {
 					staged = Staged.ADDED;
 				} else {
-					long indexEntryLastModified = indexEntry.getLastModified();
-					long resourceLastModified = resource.getLocalTimeStamp();
-
-					// C-Git under Windows stores timestamps with 1-seconds
-					// resolution, so we need to check to see if this is the
-					// case here, and possibly fix the timestamp of the resource
-					// to match the resolution of the index.
-					if (indexEntryLastModified % 1000 == 0) {
-						resourceLastModified -= resourceLastModified % 1000;
-					}
-
-					if (resourceLastModified != indexEntryLastModified) {
+					if (!timestampMatches(indexIterator, resource)) {
 						// TODO: Consider doing a content check here, to rule
 						// out false positives, as we might get mismatch between
 						// timestamps, even if the content is the same
@@ -290,37 +432,27 @@ private void extractFileProperties() throws IOException {
 					}
 				}
 			}
-
 		}
 
 		private void extractContainerProperties() throws IOException {
 			TreeWalk treeWalk = createHeadVsIndexTreeWalk();
-			if (treeWalk == null)
-				return;
-
-			if (treeWalk.next())
-				tracked = true;
+			treeWalk.setFilter(AndTreeFilter.create(treeWalk.getFilter(),
+					ContainerDiffFilter.INSTANCE));
+			treeWalk.setRecursive(true);
+
+			if (repository.getWorkDir().equals(
+					resource.getProject().getLocation().toFile()))
+				treeWalk.addTree(new ContainerTreeIterator(resource
+						.getProject()));
 			else
-				return;
-
-			// TODO: Also read ignores from .git/info/excludes et al.
-			if (Team.isIgnoredHint(resource)) {
-				ignored = true;
-				return;
-			}
-
-			// TODO: Compute dirty state for folder, using ContainerTreeIterator
-			// and ContainerDiffFilter
-
-		}
-
-		private void extractProjectProperties() throws IOException {
-			branch = repository.getBranch();
-			tracked = true;
-
-			// TODO: Compute dirty state for folder, using ContainerTreeIterator
-			// and ContainerDiffFilter
+				treeWalk.addTree(new ContainerTreeIterator(resource
+						.getWorkspace().getRoot()));
 
+			// TODO: Add fallback for projects with the repository more than
+			// one parent up, for example by using a stack of DummyIterators
+			
+			if (treeWalk.next())
+				dirty = true;
 		}
 
 		/**
@@ -328,9 +460,10 @@ private void extractProjectProperties() throws IOException {
 		 * those matching the resource specified by
 		 * <code>resourceToFilterBy</code>
 		 * <p>
-		 * If the resource does not exists in the current repository, or it has
-		 * an empty path (it is the project itself), the filter is not added,
-		 * and the method returns <code>null</code>.
+		 * If the resource does not exists in the current repository, no filter
+		 * is added and the method returns <code>false</code>. If the resource
+		 * is a project, no filter is added, but the operation is considered a
+		 * success.
 		 * 
 		 * @param treeWalk
 		 *            the tree walk to add the filter to
@@ -344,9 +477,12 @@ private boolean addResourceFilter(final TreeWalk treeWalk,
 				final IResource resourceToFilterBy) {
 			Set<String> repositoryPaths = Collections.singleton(mapping
 					.getRepoRelativePath(resourceToFilterBy));
-			if (repositoryPaths.isEmpty() || repositoryPaths.contains(""))
+			if (repositoryPaths.isEmpty())
 				return false;
 
+			if (repositoryPaths.contains(""))
+				return true; // Project filter
+
 			treeWalk.setFilter(PathFilterGroup
 					.createFromStrings(repositoryPaths));
 			return true;
@@ -376,6 +512,31 @@ private TreeWalk createHeadVsIndexTreeWalk() throws IOException {
 			return treeWalk;
 		}
 
+		private static boolean timestampMatches(DirCacheIterator index,
+				IResource workspaceResource) {
+			return timestampMatches(index.getDirCacheEntry().getLastModified(),
+					workspaceResource.getLocalTimeStamp());
+		}
+
+		private static boolean timestampMatches(DirCacheIterator index,
+				WorkingTreeIterator workspaceResource) {
+			return timestampMatches(index.getDirCacheEntry().getLastModified(),
+					workspaceResource.getEntryLastModified());
+		}
+
+		private static boolean timestampMatches(long index,
+				long workspaceResource) {
+			// C-Git under Windows stores timestamps with 1-seconds resolution,
+			// so we need to check to see if this is the case here, and possibly
+			// fix the timestamp of the resource to match the resolution of the
+			// index.
+			if (index % 1000 == 0) {
+				return index == (workspaceResource - (workspaceResource % 1000));
+			} else {
+				return index == workspaceResource;
+			}
+		}
+
 		public String getName() {
 			return resource.getName();
 		}
@@ -555,11 +716,13 @@ else if (staged == Staged.REMOVED)
 				}
 
 				// Conflicts override everything
-				if (store.getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON)
+				if (store
+						.getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON)
 						&& resource.hasConflicts())
 					overlay = conflictImage;
 
-			} else if (store.getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
+			} else if (store
+					.getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
 				overlay = untrackedImage;
 			}
 
@@ -657,6 +820,9 @@ public static void decorate(IDecoration decoration, String format,
 	 * Perform a blanket refresh of all decorations
 	 */
 	public static void refresh() {
+		if (DEBUG_DECORATIONS)
+			debug("blanket refesh");
+
 		Display.getDefault().asyncExec(new Runnable() {
 			public void run() {
 				Activator.getDefault().getWorkbench().getDecoratorManager()
@@ -679,6 +845,9 @@ public void propertyChange(PropertyChangeEvent event) {
 		if (prop.equals(TeamUI.GLOBAL_IGNORES_CHANGED)
 				|| prop.equals(TeamUI.GLOBAL_FILE_TYPES_CHANGED)
 				|| prop.equals(Activator.DECORATORS_CHANGED)) {
+			if (DEBUG_DECORATIONS)
+				debug("property=" + prop);
+
 			postLabelEvent(new LabelProviderChangedEvent(this));
 		}
 	}
@@ -696,9 +865,20 @@ public void propertyChange(PropertyChangeEvent event) {
 	public void resourceChanged(IResourceChangeEvent event) {
 		final Set<IResource> resourcesToUpdate = new HashSet<IResource>();
 
+		if (DEBUG_DECORATIONS)
+			debug("IResourceChangeEvent");
+
 		try { // Compute the changed resources by looking at the delta
 			event.getDelta().accept(new IResourceDeltaVisitor() {
 				public boolean visit(IResourceDelta delta) throws CoreException {
+
+					// If the file has changed but not in a way that we care
+					// about (e.g. marker changes to files) then ignore
+					if (delta.getKind() == IResourceDelta.CHANGED
+							&& (delta.getFlags() & INTERESTING_CHANGES) == 0) {
+						return true;
+					}
+
 					final IResource resource = delta.getResource();
 
 					// If the resource is not part of a project under Git
@@ -709,6 +889,11 @@ public boolean visit(IResourceDelta delta) throws CoreException {
 						// Ignore the change
 						return true;
 					}
+
+					if (DEBUG_DECORATIONS)
+						debug("Git resource: "
+								+ resource.getLocation().toString(), 1);
+
 					if (resource.getType() == IResource.ROOT) {
 						// Continue with the delta
 						return true;
@@ -720,16 +905,10 @@ public boolean visit(IResourceDelta delta) throws CoreException {
 							return false;
 					}
 
-					// If the file has changed but not in a way that we care
-					// about
-					// (e.g. marker changes to files) then ignore the change
-					if (delta.getKind() == IResourceDelta.CHANGED
-							&& (delta.getFlags() & INTERESTING_CHANGES) == 0) {
-						return true;
-					}
-
 					// All seems good, schedule the resource for update
 					resourcesToUpdate.add(resource);
+					if (DEBUG_DECORATIONS)
+						debug("Will update this resource", 2);
 					return true;
 				}
 			}, true /* includePhantoms */);
@@ -737,19 +916,25 @@ public boolean visit(IResourceDelta delta) throws CoreException {
 			handleException(null, e);
 		}
 
-		// If deep decorator calculation is enabled in the preferences we
-		// walk the ancestor tree of each of the changed resources and add
+		if (resourcesToUpdate.isEmpty())
+			return;
+
+		// If ancestor-decoration is enabled in the preferences we walk
+		// the ancestor tree of each of the changed resources and add
 		// their parents to the update set
 		final IPreferenceStore store = Activator.getDefault()
 				.getPreferenceStore();
-		if (store.getBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY)) {
+		if (store.getBoolean(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS)) {
+			if (DEBUG_DECORATIONS)
+				debug("Calulcating dirty ancestor..", 1);
+
 			final IResource[] changedResources = resourcesToUpdate
 					.toArray(new IResource[resourcesToUpdate.size()]);
-			for (int i = 0; i < changedResources.length; i++) {
-				IResource current = changedResources[i];
+			for (IResource current : changedResources) {
 				while (current.getType() != IResource.ROOT) {
 					current = current.getParent();
-					resourcesToUpdate.add(current);
+					if (resourcesToUpdate.add(current) && DEBUG_DECORATIONS)
+						debug("Will also update " + current.getLocation(), 2);
 				}
 			}
 		}
@@ -768,6 +953,9 @@ postLabelEvent(new LabelProviderChangedEvent(this, resourcesToUpdate
 	 *            The original change event
 	 */
 	private void repositoryChanged(RepositoryChangedEvent e) {
+		if (DEBUG_DECORATIONS)
+			debug("RepositoryChangedEvent=" + e.getRepository().getWorkDir());
+
 		final Set<RepositoryMapping> ms = new HashSet<RepositoryMapping>();
 		for (final IProject p : ResourcesPlugin.getWorkspace().getRoot()
 				.getProjects()) {
@@ -809,6 +997,9 @@ public void refsChanged(RefsChangedEvent e) {
 	 * @see org.spearce.egit.core.project.RepositoryChangeListener#repositoryChanged(org.spearce.egit.core.project.RepositoryMapping)
 	 */
 	public void repositoryChanged(RepositoryMapping mapping) {
+		if (DEBUG_DECORATIONS)
+			debug("RepositoryMapping=" + mapping.getWorkDir());
+
 		// Until we find a way to refresh visible labels within a project
 		// we have to use this blanket refresh that includes all projects.
 		postLabelEvent(new LabelProviderChangedEvent(this));
@@ -845,6 +1036,9 @@ private static IResource getResource(Object element) {
 	 *            The event to post
 	 */
 	private void postLabelEvent(final LabelProviderChangedEvent event) {
+		if (DEBUG_DECORATIONS)
+			debug("event=" + event.getElements());
+
 		Display.getDefault().asyncExec(new Runnable() {
 			public void run() {
 				fireLabelProviderChanged(event);
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java
index 74bc209..1a13a15 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java
@@ -89,7 +89,9 @@
 
 	private Text projectTextFormat;
 
-	private Button computeDeepDirtyState;
+	private Button recomputeAncestorDecorations;
+
+	private Button computeDeepDirtyStateForContainers;
 
 	private Button showTracked;
 
@@ -204,8 +206,10 @@ private Control createGeneralDecoratorPage(Composite parent) {
 		data.horizontalAlignment = GridData.FILL;
 		composite.setLayoutData(data);
 
-		computeDeepDirtyState = SWTUtils.createCheckBox(composite,
-				UIText.DecoratorPreferencesPage_computeDeep);
+		recomputeAncestorDecorations = SWTUtils.createCheckBox(composite,
+				UIText.DecoratorPreferencesPage_recomputeAncestorDecorations);
+		computeDeepDirtyStateForContainers = SWTUtils.createCheckBox(composite,
+				UIText.DecoratorPreferencesPage_computeDeepDirtyState);
 
 		return composite;
 	}
@@ -317,8 +321,10 @@ public void handleEvent(Event event) {
 	private void initializeValues() {
 		final IPreferenceStore store = getPreferenceStore();
 
-		computeDeepDirtyState.setSelection(store
-				.getBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY));
+		recomputeAncestorDecorations.setSelection(store
+				.getBoolean(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS));
+		computeDeepDirtyStateForContainers.setSelection(store
+				.getBoolean(UIPreferences.DECORATOR_COMPUTE_DEEP_DIRTY));
 
 		fileTextFormat.setText(store
 				.getString(UIPreferences.DECORATOR_FILETEXT_DECORATION));
@@ -342,7 +348,9 @@ public void widgetSelected(SelectionEvent e) {
 			}
 		};
 
-		computeDeepDirtyState.addSelectionListener(selectionListener);
+		recomputeAncestorDecorations.addSelectionListener(selectionListener);
+		computeDeepDirtyStateForContainers
+				.addSelectionListener(selectionListener);
 		showTracked.addSelectionListener(selectionListener);
 		showUntracked.addSelectionListener(selectionListener);
 		showStaged.addSelectionListener(selectionListener);
@@ -384,8 +392,10 @@ public boolean performOk() {
 	 */
 	private boolean performOk(IPreferenceStore store) {
 
-		store.setValue(UIPreferences.DECORATOR_CALCULATE_DIRTY,
-				computeDeepDirtyState.getSelection());
+		store.setValue(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS,
+				recomputeAncestorDecorations.getSelection());
+		store.setValue(UIPreferences.DECORATOR_COMPUTE_DEEP_DIRTY,
+				computeDeepDirtyStateForContainers.getSelection());
 
 		store.setValue(UIPreferences.DECORATOR_FILETEXT_DECORATION,
 				fileTextFormat.getText());
@@ -414,8 +424,11 @@ protected void performDefaults() {
 		super.performDefaults();
 		IPreferenceStore store = getPreferenceStore();
 
-		computeDeepDirtyState.setSelection(store
-				.getDefaultBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY));
+		recomputeAncestorDecorations
+				.setSelection(store
+						.getDefaultBoolean(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS));
+		computeDeepDirtyStateForContainers.setSelection(store
+				.getDefaultBoolean(UIPreferences.DECORATOR_COMPUTE_DEEP_DIRTY));
 
 		fileTextFormat.setText(store
 				.getDefaultString(UIPreferences.DECORATOR_FILETEXT_DECORATION));
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties b/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties
index 2762d11..770de4a 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties
@@ -344,7 +344,8 @@ Decorator_exceptionMessage=Errors occurred while applying Git decorations to res
 
 DecoratorPreferencesPage_addVariablesTitle=Add Variables
 DecoratorPreferencesPage_addVariablesAction=Add &Variables...
-DecoratorPreferencesPage_computeDeep=Include &ancestors when re-decorating changed resources
+DecoratorPreferencesPage_recomputeAncestorDecorations=Also re-decorate &ancestors when decorating changed resources
+DecoratorPreferencesPage_computeDeepDirtyState=Inspect dirty state of &children when decorating folders and projects
 DecoratorPreferencesPage_description=Shows Git specific information on resources in projects under version control.
 
 DecoratorPreferencesPage_decorationSettings=Decoration &settings:
-- 
1.6.1.2.309.g2ea3