Subject: [EGIT PATCH 09/11] Implement decorations of dirty, staged, and conflicting resources

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

Signed-off-by: Tor Arne Vestbø <[email-masked]>
---
 org.spearce.egit.ui/icons/ovr/assumevalid.gif      |  Bin 64 -> 0 bytes
 org.spearce.egit.ui/icons/ovr/conflict.gif         |  Bin 64 -> 164 bytes
 org.spearce.egit.ui/icons/ovr/pending_add.gif      |  Bin 64 -> 0 bytes
 org.spearce.egit.ui/icons/ovr/pending_remove.gif   |  Bin 111 -> 0 bytes
 org.spearce.egit.ui/icons/ovr/staged.gif           |  Bin 0 -> 114 bytes
 org.spearce.egit.ui/icons/ovr/staged_added.gif     |  Bin 0 -> 114 bytes
 org.spearce.egit.ui/icons/ovr/staged_removed.gif   |  Bin 0 -> 114 bytes
 .../egit/ui/PluginPreferenceInitializer.java       |    2 +
 .../src/org/spearce/egit/ui/UIIcons.java           |   13 +-
 .../src/org/spearce/egit/ui/UIPreferences.java     |    4 +
 .../src/org/spearce/egit/ui/UIText.java            |   14 +-
 .../decorators/GitLightweightDecorator.java        |  284 ++++++++++++++++----
 .../internal/decorators/IDecoratableResource.java  |   39 +++
 .../preferences/GitDecoratorPreferencePage.java    |  131 ++++++++--
 .../src/org/spearce/egit/ui/uitext.properties      |   14 +-
 15 files changed, 427 insertions(+), 74 deletions(-)
 delete mode 100644 org.spearce.egit.ui/icons/ovr/assumevalid.gif
 delete mode 100644 org.spearce.egit.ui/icons/ovr/pending_add.gif
 delete mode 100644 org.spearce.egit.ui/icons/ovr/pending_remove.gif
 create mode 100644 org.spearce.egit.ui/icons/ovr/staged.gif
 create mode 100644 org.spearce.egit.ui/icons/ovr/staged_added.gif
 create mode 100644 org.spearce.egit.ui/icons/ovr/staged_removed.gif

diff --git a/org.spearce.egit.ui/icons/ovr/assumevalid.gif b/org.spearce.egit.ui/icons/ovr/assumevalid.gif
deleted file mode 100644
index c7262ed4e3f9437a51806f70fbc851e3a6f951d3..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 64
zcmZ?wbhEHbWM|-D_{abP{~H+o{|7M?f3h$#FfcRdfH)v|1}4Ed_6CP$GF;`pupzry
Lj9oc|fx#L8z0(dE

diff --git a/org.spearce.egit.ui/icons/ovr/conflict.gif b/org.spearce.egit.ui/icons/ovr/conflict.gif
index b444be94a2d09561b212138b1514d5c07610cc07..186345711bb5443b440c4265fe62b099cce5def0 100644
GIT binary patch
literal 164
zcmZ?wbhEHbWM|-DIKseS;8EwAJi|S8rcc&9znlfG$<y4^W_f1J^~{*<m%Cuej;o77
z=*sb{&yHVx`snqS|6ssCG*JA>!pOiN$e;t#0kV^URWU)OPmsZ3MW&ETlG9;j{ud8T
Wc$>JmHcJQuD+C104Qdczum%A1Q!l9i

literal 64
zcmZ?wbhEHbWM|-D_{hM}z`*dI0SXj<vM@3*Ffr(W_#k-(Cc!EF3&kHON*Qck_+rIG
KhIgS14AuZ~{td7I

diff --git a/org.spearce.egit.ui/icons/ovr/pending_add.gif b/org.spearce.egit.ui/icons/ovr/pending_add.gif
deleted file mode 100644
index f2306024b2872e50db4143a790bca93189ef8d5f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 64
zcmZ?wbhEHbWM|-Dn8?6je1?HxCd2>#{}~t<6o0a?iZC!S=m6OaAbAER!723%B?Oj5
R<v1{ie-xgi8_2+54FJ_W4pRUC

diff --git a/org.spearce.egit.ui/icons/ovr/pending_remove.gif b/org.spearce.egit.ui/icons/ovr/pending_remove.gif
deleted file mode 100644
index 4ecce038e66f904af8345f88c2c007706a4bc3d0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 111
zcmZ?wbhEHbWM|-D*v!E2;Qr&bwzj&uy5i#Ew6wIy$jG3eAU{99|Ns9pU;~OjSy)AY
sT6I8@ATt<PWC|uQx@~OiE8rFvPw;68lw@ATcdKBBRM#03Aw~vk0Ht*t9{>OV

diff --git a/org.spearce.egit.ui/icons/ovr/staged.gif b/org.spearce.egit.ui/icons/ovr/staged.gif
new file mode 100644
index 0000000000000000000000000000000000000000..dc0b8c01673b32fde0f9b11c8302ad1ca0a1e902
GIT binary patch
literal 114
zcmZ?wbhEHb<Y3@n*v!E2>(@D15q3p!E=^fpC2=k-SzZ$rK|38WV-+D!6DfNgu|P{%
zPh+VN3xogv|1&T!DE?$&WMJT9&;hc6CU7t?u*hFHX*qrG>5bPI{QDmDwl|kFq+1-m
RA=-DKlJ_A?g`5k6H30u&A6)<d

literal 0
HcmV?d00001

diff --git a/org.spearce.egit.ui/icons/ovr/staged_added.gif b/org.spearce.egit.ui/icons/ovr/staged_added.gif
new file mode 100644
index 0000000000000000000000000000000000000000..f2b87e274667c441897d0853e6a437ba5487f51c
GIT binary patch
literal 114
zcmZ?wbhEHb<Y3@n*v!E2>(@D15q3p!E=^fpC2=k-SzZ$rK|38WV-+D!6DfNgu|P{%
zPh+VN3xogv|1&T!DE?$&WMJT9&;hc6CU7t?u*hFHX*qrG>5bPI{QC}7o_(m`FiYy3
RK~byn9^Qv66>=^N)&TbU9;*NV

literal 0
HcmV?d00001

diff --git a/org.spearce.egit.ui/icons/ovr/staged_removed.gif b/org.spearce.egit.ui/icons/ovr/staged_removed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..b6b4911d9eba72838d54fbcf6109eaf3836bf0db
GIT binary patch
literal 114
zcmZ?wbhEHb<Y3@n*v!E2>(@D15q3p!E=^fpC2=k-SzZ$rK|38WV-+D!6DfNgu|P{%
zPh+VN3xogv|1&T!DE?$&WMJT9&;hc6CU7t?u*hFHX*qrG>5bPI{QC~|wl_0qWLX?O
RA=-DKlJ_A?g`5k6H304c9%KLj

literal 0
HcmV?d00001

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 7465444..ef886cf 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
@@ -44,6 +44,8 @@ public void initializeDefaultPreferences() {
 				UIText.DecoratorPreferencesPage_projectFormatDefault);
 		prefs.setDefault(UIPreferences.DECORATOR_SHOW_TRACKED_ICON, true);
 		prefs.setDefault(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON, true);
+		prefs.setDefault(UIPreferences.DECORATOR_SHOW_STAGED_ICON, true);
+		prefs.setDefault(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON, true);
 
 		w = new int[] { 500, 500 };
 		UIPreferences.setDefault(prefs,
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIIcons.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIIcons.java
index 0cead29..302637e 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIIcons.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIIcons.java
@@ -18,11 +18,15 @@
  * Icons for the the Eclipse plugin. Mostly decorations.
  */
 public class UIIcons {
+
+	/** Decoration for resource in the index but not yet committed. */
+	public static final ImageDescriptor OVR_STAGED;
+
 	/** Decoration for resource added to index but not yet committed. */
-	public static final ImageDescriptor OVR_PENDING_ADD;
+	public static final ImageDescriptor OVR_STAGED_ADD;
 
 	/** Decoration for resource removed from the index but not commit. */
-	public static final ImageDescriptor OVR_PENDING_REMOVE;
+	public static final ImageDescriptor OVR_STAGED_REMOVE;
 
 	/** Decoration for resource not being tracked by Git */
 	public static final ImageDescriptor OVR_UNTRACKED;
@@ -75,8 +79,9 @@
 
 	static {
 		base = init();
-		OVR_PENDING_ADD = map("ovr/pending_add.gif");
-		OVR_PENDING_REMOVE = map("ovr/pending_remove.gif");
+		OVR_STAGED = map("ovr/staged.gif");
+		OVR_STAGED_ADD = map("ovr/staged_added.gif");
+		OVR_STAGED_REMOVE = map("ovr/staged_removed.gif");
 		OVR_UNTRACKED = map("ovr/untracked.gif");
 		OVR_CONFLICT = map("ovr/conflict.gif");
 		OVR_ASSUMEVALID = map("ovr/assumevalid.gif");
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 7916cea..409b335 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
@@ -64,6 +64,10 @@
 	public final static String DECORATOR_SHOW_TRACKED_ICON = "decorator_show_tracked_icon";
 	/** */
 	public final static String DECORATOR_SHOW_UNTRACKED_ICON = "decorator_show_untracked_icon";
+	/** */
+	public final static String DECORATOR_SHOW_STAGED_ICON = "decorator_show_staged_icon";
+	/** */
+	public final static String DECORATOR_SHOW_CONFLICTS_ICON = "decorator_show_conflicts_icon";
 
 	/**
 	 * Get the preference values associated with a fixed integer array.
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 fe62d0a..b62ca4c 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
@@ -953,12 +953,18 @@
 	public static String DecoratorPreferencesPage_generalTabFolder;
 
 	/** */
-	public static String DecoratorPreferencesPage_nameResourceVariable;
+	public static String DecoratorPreferencesPage_bindingResourceName;
 
 	/** */
 	public static String DecoratorPreferencesPage_bindingBranchName;
 
 	/** */
+	public static String DecoratorPreferencesPage_bindingDirtyFlag;
+
+	/** */
+	public static String DecoratorPreferencesPage_bindingStagedFlag;
+
+	/** */
 	public static String DecoratorPreferencesPage_selectFormats;
 
 	/** */
@@ -979,6 +985,12 @@
 	/** */
 	public static String DecoratorPreferencesPage_iconsShowUntracked;
 
+	/** */
+	public static String DecoratorPreferencesPage_iconsShowStaged;
+
+	/** */
+	public static String DecoratorPreferencesPage_iconsShowConflicts;
+
 	static {
 		initializeMessages(UIText.class.getPackage().getName() + ".uitext",
 				UIText.class);
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 b20070a..aa6a261 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
@@ -60,9 +60,12 @@
 import org.spearce.egit.ui.UIIcons;
 import org.spearce.egit.ui.UIPreferences;
 import org.spearce.egit.ui.UIText;
+import org.spearce.egit.ui.internal.decorators.IDecoratableResource.Staged;
 import org.spearce.jgit.dircache.DirCache;
+import org.spearce.jgit.dircache.DirCacheEntry;
 import org.spearce.jgit.dircache.DirCacheIterator;
 import org.spearce.jgit.lib.Constants;
+import org.spearce.jgit.lib.FileMode;
 import org.spearce.jgit.lib.IndexChangedEvent;
 import org.spearce.jgit.lib.ObjectId;
 import org.spearce.jgit.lib.RefsChangedEvent;
@@ -200,6 +203,18 @@ public void decorate(Object element, IDecoration decoration) {
 
 		private boolean ignored = false;
 
+		private boolean dirty = false;
+
+		private boolean conflicts = false;
+
+		private Staged staged = Staged.NOT_STAGED;
+
+		static final int T_HEAD = 0;
+
+		static final int T_INDEX = 1;
+
+		static final int T_WORKSPACE = 2;
+
 		public DecoratableResourceAdapter(IResource resourceToWrap)
 				throws IOException {
 			resource = resourceToWrap;
@@ -207,59 +222,161 @@ public DecoratableResourceAdapter(IResource resourceToWrap)
 			repository = mapping.getRepository();
 			headId = repository.resolve(Constants.HEAD);
 
-			initializeValues();
+			switch (resource.getType()) {
+			case IResource.FILE:
+				extractFileProperties();
+				break;
+			case IResource.FOLDER:
+				extractContainerProperties();
+				break;
+			case IResource.PROJECT:
+				extractProjectProperties();
+				break;
+			}
 		}
 
-		/**
-		 * Initialize the various values that are used for making decoration
-		 * decisions later on.
-		 * 
-		 * We might as well pre-load these now, instead of using lazy
-		 * initialization, because they are all read by the decorator when
-		 * building variable bindings and computing the preferred overlay.
-		 * 
-		 * @throws IOException
-		 */
-		private void initializeValues() throws IOException {
-
-			// Resolve current branch
-			branch = repository.getBranch();
+		private void extractFileProperties() throws IOException {
+			TreeWalk treeWalk = createHeadVsIndexTreeWalk();
+			if (treeWalk == null)
+				return;
 
-			// Resolve tracked state
-			if (getType() == IResource.PROJECT) {
+			if (treeWalk.next())
 				tracked = true;
+			else
+				return;
+
+			// TODO: Also read ignores from .git/info/excludes et al.
+			if (Team.isIgnoredHint(resource)) {
+				ignored = true;
+				return;
+			}
+
+			final DirCacheIterator indexIterator = treeWalk.getTree(T_INDEX,
+					DirCacheIterator.class);
+			final DirCacheEntry indexEntry = indexIterator != null ? indexIterator
+					.getDirCacheEntry()
+					: null;
+
+			if (indexEntry == null) {
+				staged = Staged.REMOVED;
 			} else {
-				final TreeWalk treeWalk = new TreeWalk(repository);
-
-				Set<String> repositoryPaths = Collections.singleton(mapping
-						.getRepoRelativePath(resource));
-				if (!(repositoryPaths.isEmpty() || repositoryPaths.contains(""))) {
-					treeWalk.setFilter(PathFilterGroup
-							.createFromStrings(repositoryPaths));
-					treeWalk.setRecursive(treeWalk.getFilter()
-							.shouldBeRecursive());
-					treeWalk.reset();
-
-					if (headId != null)
-						treeWalk.addTree(new RevWalk(repository)
-								.parseTree(headId));
-					else
-						treeWalk.addTree(new EmptyTreeIterator());
+				if (indexEntry.isAssumeValid()) {
+					dirty = false;
+				} else if (indexEntry.getStage() > 0) {
+					conflicts = true;
+				} else if (treeWalk.getRawMode(T_HEAD) == FileMode.MISSING
+						.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) {
+						// 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
+						dirty = true;
+					}
 
-					treeWalk.addTree(new DirCacheIterator(DirCache
-							.read(repository)));
-					if (treeWalk.next()) {
-						tracked = true;
+					if (treeWalk.getRawMode(T_HEAD) != treeWalk
+							.getRawMode(T_INDEX)
+							|| !treeWalk.idEqual(T_HEAD, T_INDEX)) {
+						staged = Staged.MODIFIED;
 					}
 				}
 			}
 
-			// Resolve ignored state (currently only reads the global Eclipse
-			// ignores)
+		}
+
+		private void extractContainerProperties() throws IOException {
+			TreeWalk treeWalk = createHeadVsIndexTreeWalk();
+			if (treeWalk == null)
+				return;
+
+			if (treeWalk.next())
+				tracked = true;
+			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
+
+		}
+
+		/**
+		 * Adds a filter to the specified tree walk limiting the results to only
+		 * 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>.
+		 * 
+		 * @param treeWalk
+		 *            the tree walk to add the filter to
+		 * @param resourceToFilterBy
+		 *            the resource to filter by
+		 * 
+		 * @return <code>true</code> if the filter could be added,
+		 *         <code>false</code> otherwise
+		 */
+		private boolean addResourceFilter(final TreeWalk treeWalk,
+				final IResource resourceToFilterBy) {
+			Set<String> repositoryPaths = Collections.singleton(mapping
+					.getRepoRelativePath(resourceToFilterBy));
+			if (repositoryPaths.isEmpty() || repositoryPaths.contains(""))
+				return false;
+
+			treeWalk.setFilter(PathFilterGroup
+					.createFromStrings(repositoryPaths));
+			return true;
+		}
+
+		/**
+		 * Helper method to create a new tree walk between HEAD and the index.
+		 * 
+		 * @return the created tree walk, or null if it could not be created
+		 * @throws IOException
+		 *             if there were errors when creating the tree walk
+		 */
+		private TreeWalk createHeadVsIndexTreeWalk() throws IOException {
+			final TreeWalk treeWalk = new TreeWalk(repository);
+			if (!addResourceFilter(treeWalk, resource))
+				return null;
+
+			treeWalk.setRecursive(treeWalk.getFilter().shouldBeRecursive());
+			treeWalk.reset();
+
+			if (headId != null)
+				treeWalk.addTree(new RevWalk(repository).parseTree(headId));
+			else
+				treeWalk.addTree(new EmptyTreeIterator());
+
+			treeWalk.addTree(new DirCacheIterator(DirCache.read(repository)));
+			return treeWalk;
 		}
 
 		public String getName() {
@@ -281,6 +398,18 @@ public boolean isTracked() {
 		public boolean isIgnored() {
 			return ignored;
 		}
+
+		public boolean isDirty() {
+			return dirty;
+		}
+
+		public Staged staged() {
+			return staged;
+		}
+
+		public boolean hasConflicts() {
+			return conflicts;
+		}
 	}
 
 	/**
@@ -298,6 +427,12 @@ public boolean isIgnored() {
 		/** */
 		public static final String BINDING_BRANCH_NAME = "branch"; //$NON-NLS-1$
 
+		/** */
+		public static final String BINDING_DIRTY_FLAG = "dirty"; //$NON-NLS-1$
+
+		/** */
+		public static final String BINDING_STAGED_FLAG = "staged"; //$NON-NLS-1$
+
 		private IPreferenceStore store;
 
 		/**
@@ -325,10 +460,23 @@ public ImageData getImageData() {
 
 		private static ImageDescriptor untrackedImage;
 
+		private static ImageDescriptor stagedImage;
+
+		private static ImageDescriptor stagedAddedImage;
+
+		private static ImageDescriptor stagedRemovedImage;
+
+		private static ImageDescriptor conflictImage;
+
 		static {
 			trackedImage = new CachedImageDescriptor(TeamImages
 					.getImageDescriptor(ISharedImages.IMG_CHECKEDIN_OVR));
 			untrackedImage = new CachedImageDescriptor(UIIcons.OVR_UNTRACKED);
+			stagedImage = new CachedImageDescriptor(UIIcons.OVR_STAGED);
+			stagedAddedImage = new CachedImageDescriptor(UIIcons.OVR_STAGED_ADD);
+			stagedRemovedImage = new CachedImageDescriptor(
+					UIIcons.OVR_STAGED_REMOVE);
+			conflictImage = new CachedImageDescriptor(UIIcons.OVR_CONFLICT);
 		}
 
 		/**
@@ -354,6 +502,9 @@ public DecorationHelper(IPreferenceStore preferencesStore) {
 		 */
 		public void decorate(IDecoration decoration,
 				IDecoratableResource resource) {
+			if (resource.isIgnored())
+				return;
+
 			decorateText(decoration, resource);
 			decorateIcons(decoration, resource);
 		}
@@ -379,22 +530,44 @@ private void decorateText(IDecoration decoration,
 			Map<String, String> bindings = new HashMap<String, String>();
 			bindings.put(BINDING_RESOURCE_NAME, resource.getName());
 			bindings.put(BINDING_BRANCH_NAME, resource.getBranch());
+			bindings.put(BINDING_DIRTY_FLAG, resource.isDirty() ? ">" : null);
+			bindings.put(BINDING_STAGED_FLAG,
+					resource.staged() != Staged.NOT_STAGED ? "*" : null);
 
 			decorate(decoration, format, bindings);
 		}
 
 		private void decorateIcons(IDecoration decoration,
 				IDecoratableResource resource) {
-			if (resource.isIgnored())
-				return;
+			ImageDescriptor overlay = null;
 
 			if (resource.isTracked()) {
 				if (store.getBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON))
-					decoration.addOverlay(trackedImage);
-			} else if (store
-					.getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
-				decoration.addOverlay(untrackedImage);
+					overlay = trackedImage;
+
+				// Staged overrides tracked
+				Staged staged = resource.staged();
+				if (store.getBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON)
+						&& staged != Staged.NOT_STAGED) {
+					if (staged == Staged.ADDED)
+						overlay = stagedAddedImage;
+					else if (staged == Staged.REMOVED)
+						overlay = stagedRemovedImage;
+					else
+						overlay = stagedImage;
+				}
+
+				// Conflicts override everything
+				if (store.getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON)
+						&& resource.hasConflicts())
+					overlay = conflictImage;
+
+			} else if (store.getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
+				overlay = untrackedImage;
 			}
+
+			// Overlays can only be added once, so do it at the end
+			decoration.addOverlay(overlay);
 		}
 
 		/**
@@ -411,7 +584,7 @@ private void decorateIcons(IDecoration decoration,
 		 *            values
 		 */
 		public static void decorate(IDecoration decoration, String format,
-				Map bindings) {
+				Map<String, String> bindings) {
 			StringBuffer prefix = new StringBuffer();
 			StringBuffer suffix = new StringBuffer();
 			StringBuffer output = prefix;
@@ -426,6 +599,15 @@ public static void decorate(IDecoration decoration, String format,
 						String key = format.substring(end + 1, start);
 						String s;
 
+						// Allow users to override the binding
+						if (key.indexOf(':') > -1) {
+							String[] keyAndBinding = key.split(":", 2);
+							key = keyAndBinding[0];
+							if (keyAndBinding.length > 1
+									&& bindings.get(key) != null)
+								bindings.put(key, keyAndBinding[1]);
+						}
+
 						// We use the BINDING_RESOURCE_NAME key to determine if
 						// we are doing the prefix or suffix. The name isn't
 						// actually part of either.
@@ -433,7 +615,7 @@ public static void decorate(IDecoration decoration, String format,
 							output = suffix;
 							s = null;
 						} else {
-							s = (String) bindings.get(key);
+							s = bindings.get(key);
 						}
 
 						if (s != null) {
@@ -522,6 +704,14 @@ public void resourceChanged(IResourceChangeEvent event) {
 				public boolean visit(IResourceDelta delta) throws CoreException {
 					final IResource resource = delta.getResource();
 
+					// If the resource is not part of a project under Git
+					// revision control
+					final RepositoryMapping mapping = RepositoryMapping
+							.getMapping(resource);
+					if (mapping == null) {
+						// Ignore the change
+						return true;
+					}
 					if (resource.getType() == IResource.ROOT) {
 						// Continue with the delta
 						return true;
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java
index f144214..d2b6b5c 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java
@@ -16,6 +16,22 @@
 public interface IDecoratableResource {
 
 	/**
+	 * Set of possible staging states for a resource
+	 */
+	public enum Staged {
+		/** Represents a resource that is not staged */
+		NOT_STAGED,
+		/** Represents a resource that has been modified */
+		MODIFIED,
+		/** Represents a resource that is added to Git */
+		ADDED,
+		/** Represents a resource that is removed from Git */
+		REMOVED,
+		/** Represents a resource that has been renamed */
+		RENAMED
+	}
+
+	/**
 	 * Gets the type of the resource as defined by {@link IResource}
 	 * 
 	 * @return the type of the resource
@@ -51,4 +67,27 @@
 	 * @return whether or not the resource is ignored
 	 */
 	boolean isIgnored();
+
+	/**
+	 * Returns whether or not the resource has changes that are not staged
+	 * 
+	 * @return whether or not the resource is dirty
+	 */
+	boolean isDirty();
+
+	/**
+	 * Returns the staged state of the resource
+	 * 
+	 * The set of allowed values are defined by the <code>Staged</code> enum
+	 * 
+	 * @return the staged state of the resource
+	 */
+	Staged staged();
+
+	/**
+	 * Returns whether or not the resource has merge conflicts
+	 * 
+	 * @return whether or not the resource has merge conflicts
+	 */
+	boolean hasConflicts();
 }
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 2cbf07a..74bc209 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
@@ -56,6 +56,7 @@
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.TabFolder;
 import org.eclipse.swt.widgets.TabItem;
@@ -72,8 +73,9 @@
 import org.spearce.egit.ui.UIPreferences;
 import org.spearce.egit.ui.UIText;
 import org.spearce.egit.ui.internal.SWTUtils;
-import org.spearce.egit.ui.internal.decorators.GitLightweightDecorator.DecorationHelper;
 import org.spearce.egit.ui.internal.decorators.IDecoratableResource;
+import org.spearce.egit.ui.internal.decorators.GitLightweightDecorator.DecorationHelper;
+import org.spearce.egit.ui.internal.decorators.IDecoratableResource.Staged;
 
 /**
  * Preference page for customizing Git label decorations
@@ -93,6 +95,10 @@
 
 	private Button showUntracked;
 
+	private Button showStaged;
+
+	private Button showConflicts;
+
 	private Preview fPreview;
 
 	private static final Collection PREVIEW_FILESYSTEM_ROOT;
@@ -101,16 +107,29 @@
 
 	static {
 		final PreviewResource project = new PreviewResource(
-				"Project", IResource.PROJECT, "master", true, false); //$NON-NLS-1$1
+				"Project", IResource.PROJECT, "master", true, false, false, Staged.NOT_STAGED, false); //$NON-NLS-1$1
 		final ArrayList<PreviewResource> children = new ArrayList<PreviewResource>();
+
+		children.add(new PreviewResource(
+						"folder", IResource.FOLDER, null, true, false, false, Staged.NOT_STAGED, false)); //$NON-NLS-1$
+		children.add(new PreviewResource(
+						"tracked.txt", IResource.FILE, null, true, false, false, Staged.NOT_STAGED, false)); //$NON-NLS-1$
 		children.add(new PreviewResource(
-				"folder", IResource.FOLDER, null, true, false)); //$NON-NLS-1$
+						"untracked.txt", IResource.FILE, null, false, false, false, Staged.NOT_STAGED, false)); //$NON-NLS-1$
 		children.add(new PreviewResource(
-				"file.txt", IResource.FILE, null, true, false)); //$NON-NLS-1$
+						"ignored.txt", IResource.FILE, null, false, true, false, Staged.NOT_STAGED, false)); //$NON-NLS-1$
 		children.add(new PreviewResource(
-				"untracked.txt", IResource.FILE, null, false, false)); //$NON-NLS-1$
+						"dirty.txt", IResource.FILE, null, true, false, true, Staged.NOT_STAGED, false)); //$NON-NLS-1$
 		children.add(new PreviewResource(
-				"ignored.txt", IResource.FILE, null, false, true)); //$NON-NLS-1$
+						"staged.txt", IResource.FILE, null, true, false, false, Staged.MODIFIED, false)); //$NON-NLS-1$
+		children.add(new PreviewResource(
+						"partially-staged.txt", IResource.FILE, null, true, false, true, Staged.MODIFIED, false)); //$NON-NLS-1$
+		children.add(new PreviewResource(
+						"added.txt", IResource.FILE, null, true, false, false, Staged.ADDED, false)); //$NON-NLS-1$
+		children.add(new PreviewResource(
+						"removed.txt", IResource.FILE, null, true, false, false, Staged.REMOVED, false)); //$NON-NLS-1$
+		children.add(new PreviewResource(
+						"conflict.txt", IResource.FILE, null, true, false, true, Staged.NOT_STAGED, true)); //$NON-NLS-1$
 		project.children = children;
 		PREVIEW_FILESYSTEM_ROOT = Collections.singleton(project);
 	}
@@ -179,6 +198,7 @@ private Control createGeneralDecoratorPage(Composite parent) {
 		Composite composite = new Composite(parent, SWT.NULL);
 
 		GridLayout layout = new GridLayout();
+		layout.marginHeight = 10;
 		composite.setLayout(layout);
 		GridData data = new GridData();
 		data.horizontalAlignment = GridData.FILL;
@@ -206,22 +226,29 @@ private Control createTextDecoratorPage(Composite parent) {
 		data.horizontalAlignment = GridData.FILL;
 		fileTextGroup.setLayoutData(data);
 
+		int labelWidth = convertWidthInCharsToPixels(Math.max(
+				UIText.DecoratorPreferencesPage_fileFormatLabel.length(),
+				Math.max(UIText.DecoratorPreferencesPage_folderFormatLabel
+						.length(),
+						UIText.DecoratorPreferencesPage_projectFormatLabel
+								.length())));
+
 		TextPair format = createFormatEditorControl(fileTextGroup,
 				UIText.DecoratorPreferencesPage_fileFormatLabel,
 				UIText.DecoratorPreferencesPage_addVariablesAction,
-				getFileBindingDescriptions());
+				getFileBindingDescriptions(), labelWidth);
 		fileTextFormat = format.t1;
 
 		format = createFormatEditorControl(fileTextGroup,
 				UIText.DecoratorPreferencesPage_folderFormatLabel,
 				UIText.DecoratorPreferencesPage_addVariablesAction,
-				getFolderBindingDescriptions());
+				getFolderBindingDescriptions(), labelWidth);
 		folderTextFormat = format.t1;
 
 		format = createFormatEditorControl(fileTextGroup,
 				UIText.DecoratorPreferencesPage_projectFormatLabel,
 				UIText.DecoratorPreferencesPage_addVariablesAction,
-				getProjectBindingDescriptions());
+				getProjectBindingDescriptions(), labelWidth);
 		projectTextFormat = format.t1;
 
 		return fileTextGroup;
@@ -230,6 +257,7 @@ private Control createTextDecoratorPage(Composite parent) {
 	private Control createIconDecoratorPage(Composite parent) {
 		Composite imageGroup = new Composite(parent, SWT.NULL);
 		GridLayout layout = new GridLayout();
+		layout.marginHeight = 10;
 		imageGroup.setLayout(layout);
 		GridData data = new GridData();
 		data.horizontalAlignment = GridData.FILL;
@@ -239,17 +267,27 @@ private Control createIconDecoratorPage(Composite parent) {
 				UIText.DecoratorPreferencesPage_iconsShowTracked);
 		showUntracked = SWTUtils.createCheckBox(imageGroup,
 				UIText.DecoratorPreferencesPage_iconsShowUntracked);
+		showStaged = SWTUtils.createCheckBox(imageGroup,
+				UIText.DecoratorPreferencesPage_iconsShowStaged);
+		showConflicts = SWTUtils.createCheckBox(imageGroup,
+				UIText.DecoratorPreferencesPage_iconsShowConflicts);
 
 		return imageGroup;
 	}
 
 	private TextPair createFormatEditorControl(Composite composite,
-			String title, String buttonText, final Map supportedBindings) {
+			String title, String buttonText, final Map supportedBindings,
+			int labelWidth) {
 
-		SWTUtils.createLabel(composite, title);
+		Label label = SWTUtils.createLabel(composite, title);
+		GridData labelGridData = new GridData();
+		labelGridData.widthHint = labelWidth;
+		label.setLayoutData(labelGridData);
 
 		Text format = new Text(composite, SWT.BORDER);
-		format.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+		GridData textGridData = new GridData(GridData.FILL_HORIZONTAL);
+		textGridData.widthHint = 200;
+		format.setLayoutData(textGridData);
 		format.addModifyListener(new ModifyListener() {
 			public void modifyText(ModifyEvent e) {
 				updatePreview();
@@ -293,6 +331,10 @@ private void initializeValues() {
 				.getBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON));
 		showUntracked.setSelection(store
 				.getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON));
+		showStaged.setSelection(store
+				.getBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON));
+		showConflicts.setSelection(store
+				.getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON));
 
 		SelectionListener selectionListener = new SelectionAdapter() {
 			public void widgetSelected(SelectionEvent e) {
@@ -303,6 +345,8 @@ public void widgetSelected(SelectionEvent e) {
 		computeDeepDirtyState.addSelectionListener(selectionListener);
 		showTracked.addSelectionListener(selectionListener);
 		showUntracked.addSelectionListener(selectionListener);
+		showStaged.addSelectionListener(selectionListener);
+		showConflicts.addSelectionListener(selectionListener);
 
 		setValid(true);
 	}
@@ -354,6 +398,10 @@ private boolean performOk(IPreferenceStore store) {
 				.getSelection());
 		store.setValue(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON,
 				showUntracked.getSelection());
+		store.setValue(UIPreferences.DECORATOR_SHOW_STAGED_ICON, showStaged
+				.getSelection());
+		store.setValue(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON,
+				showConflicts.getSelection());
 
 		return true;
 	}
@@ -383,6 +431,11 @@ protected void performDefaults() {
 		showUntracked
 				.setSelection(store
 						.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON));
+		showStaged.setSelection(store
+				.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON));
+		showConflicts
+				.setSelection(store
+						.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON));
 	}
 
 	/**
@@ -505,7 +558,11 @@ TextPair(Text t1, Text t2) {
 	private Map getFileBindingDescriptions() {
 		Map<String, String> bindings = new HashMap<String, String>();
 		bindings.put(DecorationHelper.BINDING_RESOURCE_NAME,
-				UIText.DecoratorPreferencesPage_nameResourceVariable);
+				UIText.DecoratorPreferencesPage_bindingResourceName);
+		bindings.put(DecorationHelper.BINDING_DIRTY_FLAG,
+				UIText.DecoratorPreferencesPage_bindingDirtyFlag);
+		bindings.put(DecorationHelper.BINDING_STAGED_FLAG,
+				UIText.DecoratorPreferencesPage_bindingStagedFlag);
 		return bindings;
 	}
 
@@ -518,7 +575,11 @@ private Map getFileBindingDescriptions() {
 	private Map getFolderBindingDescriptions() {
 		Map<String, String> bindings = new HashMap<String, String>();
 		bindings.put(DecorationHelper.BINDING_RESOURCE_NAME,
-				UIText.DecoratorPreferencesPage_nameResourceVariable);
+				UIText.DecoratorPreferencesPage_bindingResourceName);
+		bindings.put(DecorationHelper.BINDING_DIRTY_FLAG,
+				UIText.DecoratorPreferencesPage_bindingDirtyFlag);
+		bindings.put(DecorationHelper.BINDING_STAGED_FLAG,
+				UIText.DecoratorPreferencesPage_bindingStagedFlag);
 		return bindings;
 	}
 
@@ -531,7 +592,11 @@ private Map getFolderBindingDescriptions() {
 	private Map getProjectBindingDescriptions() {
 		Map<String, String> bindings = new HashMap<String, String>();
 		bindings.put(DecorationHelper.BINDING_RESOURCE_NAME,
-				UIText.DecoratorPreferencesPage_nameResourceVariable);
+				UIText.DecoratorPreferencesPage_bindingResourceName);
+		bindings.put(DecorationHelper.BINDING_DIRTY_FLAG,
+				UIText.DecoratorPreferencesPage_bindingDirtyFlag);
+		bindings.put(DecorationHelper.BINDING_STAGED_FLAG,
+				UIText.DecoratorPreferencesPage_bindingStagedFlag);
 		bindings.put(DecorationHelper.BINDING_BRANCH_NAME,
 				UIText.DecoratorPreferencesPage_bindingBranchName);
 		return bindings;
@@ -691,14 +756,25 @@ private PreviewDecoration getDecoration(Object element) {
 
 		private boolean ignored;
 
+		private boolean dirty;
+
+		private boolean conflicts;
+
+		private Staged staged;
+
 		public PreviewResource(String name, int type, String branch,
-				boolean tracked, boolean ignored) {
+				boolean tracked, boolean ignored, boolean dirty, Staged staged,
+				boolean conflicts) {
+
 			this.name = name;
 			this.branch = branch;
 			this.type = type;
 			this.children = Collections.EMPTY_LIST;
 			this.tracked = tracked;
 			this.ignored = ignored;
+			this.dirty = dirty;
+			this.staged = staged;
+			this.conflicts = conflicts;
 		}
 
 		public String getName() {
@@ -720,6 +796,18 @@ public boolean isTracked() {
 		public boolean isIgnored() {
 			return ignored;
 		}
+
+		public boolean isDirty() {
+			return dirty;
+		}
+
+		public Staged staged() {
+			return staged;
+		}
+
+		public boolean hasConflicts() {
+			return conflicts;
+		}
 	}
 
 	private class PreviewDecoration implements IDecoration {
@@ -736,12 +824,19 @@ public boolean isIgnored() {
 
 		private Color foregroundColor;
 
+		/**
+		 * Adds an icon overlay to the decoration
+		 * <p>
+		 * Copies the behavior of <code>DecorationBuilder</code> of only
+		 * allowing the overlay to be set once.
+		 */
 		public void addOverlay(ImageDescriptor overlayImage) {
-			overlay = overlayImage;
+			if (overlay == null)
+				overlay = overlayImage;
 		}
 
 		public void addOverlay(ImageDescriptor overlayImage, int quadrant) {
-			overlay = overlayImage;
+			addOverlay(overlayImage);
 		}
 
 		public void addPrefix(String prefix) {
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 05fdba9..2762d11 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
@@ -352,13 +352,15 @@ DecoratorPreferencesPage_preview=Preview:
 DecoratorPreferencesPage_fileFormatLabel=&Files:
 DecoratorPreferencesPage_folderFormatLabel=F&olders:
 DecoratorPreferencesPage_projectFormatLabel=&Projects:
-DecoratorPreferencesPage_fileFormatDefault={name}
-DecoratorPreferencesPage_folderFormatDefault={name}
-DecoratorPreferencesPage_projectFormatDefault={name} [{branch}]
+DecoratorPreferencesPage_fileFormatDefault={dirty:>} {name}
+DecoratorPreferencesPage_folderFormatDefault={dirty:>} {name}
+DecoratorPreferencesPage_projectFormatDefault={dirty:>} {name} [{branch}]
 DecoratorPreferencesPage_labelDecorationsLink=See <a>''{0}''</a> to enable or disable Git decorations.
 DecoratorPreferencesPage_generalTabFolder=&General
-DecoratorPreferencesPage_nameResourceVariable=name of the resource being decorated
+DecoratorPreferencesPage_bindingResourceName=name of the resource being decorated
 DecoratorPreferencesPage_bindingBranchName=current branch of the project
+DecoratorPreferencesPage_bindingDirtyFlag=flag indicating whether or not the resource is dirty
+DecoratorPreferencesPage_bindingStagedFlag=flag indicating whether or not the resource is staged
 DecoratorPreferencesPage_selectFormats=Select the format for file, folders, and project text labels:
 DecoratorPreferencesPage_selectVariablesToAdd=Select the &variables to add to the decoration format:
 DecoratorPreferencesPage_textLabel=T&ext Decorations
@@ -366,3 +368,7 @@ DecoratorPreferencesPage_iconLabel=&Icon Decorations
 DecoratorPreferencesPage_iconsShowTracked=Tracked resources
 DecoratorPreferencesPage_iconsShowUntracked=Untracked resources
 
+Decorator_exceptionMessage=Errors occurred while applying Git decorations to resources.
+
+DecoratorPreferencesPage_iconsShowStaged=Staged resources
+DecoratorPreferencesPage_iconsShowConflicts=Conflicting resources
-- 
1.6.1.2.309.g2ea3