Add Attribute Macro Expansion

Attributes MacroExpander implements macros used in git attributes. This
is implemented inside the TreeWalk using a lazy created MacroExpander.
In addition, the macro expander caches the global and info attributes
node in order to provide fast merge of attributes.

Change-Id: I2e69c9fc84e9d7fb8df0a05817d688fc456d8f00
Signed-off-by: Ivan Motsch <ivan.motsch@bsiag.com>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
new file mode 100644
index 0000000..ca456b3
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
@@ -0,0 +1,339 @@
+/*
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.attributes;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Test;
+
+/**
+ * Tests {@link AttributesHandler}
+ */
+public class AttributesHandlerTest extends RepositoryTestCase {
+	private static final FileMode D = FileMode.TREE;
+
+	private static final FileMode F = FileMode.REGULAR_FILE;
+
+	private TreeWalk walk;
+
+	@Test
+	public void testExpandNonMacro1() throws Exception {
+		setupRepo(null, null, null, "*.txt text");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("text"));
+		endWalk();
+	}
+
+	@Test
+	public void testExpandNonMacro2() throws Exception {
+		setupRepo(null, null, null, "*.txt -text");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("-text"));
+		endWalk();
+	}
+
+	@Test
+	public void testExpandNonMacro3() throws Exception {
+		setupRepo(null, null, null, "*.txt !text");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs(""));
+		endWalk();
+	}
+
+	@Test
+	public void testExpandNonMacro4() throws Exception {
+		setupRepo(null, null, null, "*.txt text=auto");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("text=auto"));
+		endWalk();
+	}
+
+	@Test
+	public void testExpandBuiltInMacro1() throws Exception {
+		setupRepo(null, null, null, "*.txt binary");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("binary -diff -merge -text"));
+		endWalk();
+	}
+
+	@Test
+	public void testExpandBuiltInMacro2() throws Exception {
+		setupRepo(null, null, null, "*.txt -binary");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("-binary diff merge text"));
+		endWalk();
+	}
+
+	@Test
+	public void testExpandBuiltInMacro3() throws Exception {
+		setupRepo(null, null, null, "*.txt !binary");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs(""));
+		endWalk();
+	}
+
+	@Test
+	public void testCustomGlobalMacro1() throws Exception {
+		setupRepo(
+				"[attr]foo a -b !c d=e", null, null, "*.txt foo");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo a -b d=e"));
+		endWalk();
+	}
+
+	@Test
+	public void testCustomGlobalMacro2() throws Exception {
+		setupRepo("[attr]foo a -b !c d=e", null, null, "*.txt -foo");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("-foo -a b d=e"));
+		endWalk();
+	}
+
+	@Test
+	public void testCustomGlobalMacro3() throws Exception {
+		setupRepo("[attr]foo a -b !c d=e", null, null, "*.txt !foo");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs(""));
+		endWalk();
+	}
+
+	@Test
+	public void testCustomGlobalMacro4() throws Exception {
+		setupRepo("[attr]foo a -b !c d=e", null, null, "*.txt foo=bar");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo=bar a -b d=bar"));
+		endWalk();
+	}
+
+	@Test
+	public void testInfoOverridesGlobal() throws Exception {
+		setupRepo("[attr]foo bar1",
+				"[attr]foo bar2", null, "*.txt foo");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo bar2"));
+		endWalk();
+	}
+
+	@Test
+	public void testWorkDirRootOverridesGlobal() throws Exception {
+		setupRepo("[attr]foo bar1",
+				null,
+				"[attr]foo bar3", "*.txt foo");
+
+		walk = beginWalk();
+		assertIteration(F, ".gitattributes");
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo bar3"));
+		endWalk();
+	}
+
+	@Test
+	public void testInfoOverridesWorkDirRoot() throws Exception {
+		setupRepo("[attr]foo bar1",
+				"[attr]foo bar2", "[attr]foo bar3", "*.txt foo");
+
+		walk = beginWalk();
+		assertIteration(F, ".gitattributes");
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo bar2"));
+		endWalk();
+	}
+
+	@Test
+	public void testRecursiveMacro() throws Exception {
+		setupRepo(
+				"[attr]foo x bar -foo",
+				null, null, "*.txt foo");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo x bar"));
+		endWalk();
+	}
+
+	@Test
+	public void testCyclicMacros() throws Exception {
+		setupRepo(
+				"[attr]foo x -bar\n[attr]bar y -foo", null, null, "*.txt foo");
+
+		walk = beginWalk();
+		assertIteration(D, "sub");
+		assertIteration(F, "sub/.gitattributes");
+		assertIteration(F, "sub/a.txt", attrs("foo x -bar -y"));
+		endWalk();
+	}
+
+	private static Collection<Attribute> attrs(String s) {
+		return new AttributesRule("*", s).getAttributes();
+	}
+
+	private void assertIteration(FileMode type, String pathName)
+			throws IOException {
+		assertIteration(type, pathName, Collections.<Attribute> emptyList());
+	}
+
+	private void assertIteration(FileMode type, String pathName,
+			Collection<Attribute> expectedAttrs) throws IOException {
+		assertTrue("walk has entry", walk.next());
+		assertEquals(pathName, walk.getPathString());
+		assertEquals(type, walk.getFileMode(0));
+
+		if (expectedAttrs != null) {
+			assertEquals(new ArrayList<>(expectedAttrs),
+					new ArrayList<>(walk.getAttributes().getAll()));
+		}
+
+		if (D.equals(type))
+			walk.enterSubtree();
+	}
+
+	/**
+	 * @param globalAttributesContent
+	 * @param infoAttributesContent
+	 * @param rootAttributesContent
+	 * @param subDirAttributesContent
+	 * @throws Exception
+	 *             Setup a repo with .gitattributes files and a test file
+	 *             sub/a.txt
+	 */
+	private void setupRepo(
+			String globalAttributesContent,
+			String infoAttributesContent, String rootAttributesContent, String subDirAttributesContent)
+					throws Exception {
+		FileBasedConfig config = db.getConfig();
+		if (globalAttributesContent != null) {
+			File f = new File(db.getDirectory(), "global/attributes");
+			write(f, globalAttributesContent);
+			config.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_KEY_ATTRIBUTESFILE,
+					f.getAbsolutePath());
+
+		}
+		if (infoAttributesContent != null) {
+			File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES);
+			write(f, infoAttributesContent);
+		}
+		config.save();
+
+		if (rootAttributesContent != null) {
+			writeAttributesFile(Constants.DOT_GIT_ATTRIBUTES,
+					rootAttributesContent);
+		}
+
+		if (subDirAttributesContent != null) {
+			writeAttributesFile("sub/" + Constants.DOT_GIT_ATTRIBUTES,
+					subDirAttributesContent);
+		}
+
+		writeTrashFile("sub/a.txt", "a");
+	}
+
+	private void writeAttributesFile(String name, String... rules)
+			throws IOException {
+		StringBuilder data = new StringBuilder();
+		for (String line : rules)
+			data.append(line + "\n");
+		writeTrashFile(name, data.toString());
+	}
+
+	private TreeWalk beginWalk() {
+		TreeWalk newWalk = new TreeWalk(db);
+		newWalk.addTree(new FileTreeIterator(db));
+		return newWalk;
+	}
+
+	private void endWalk() throws IOException {
+		assertFalse("Not all files tested", walk.next());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java
index 9f82b8a..e8dd952 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java
@@ -293,28 +293,28 @@
 	public void testGetters() {
 		AttributesRule r = new AttributesRule("/pattern/", "");
 		assertFalse(r.isNameOnly());
-		assertTrue(r.dirOnly());
+		assertTrue(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertTrue(r.getAttributes().isEmpty());
 		assertEquals(r.getPattern(), "/pattern");
 
 		r = new AttributesRule("/patter?/", "");
 		assertFalse(r.isNameOnly());
-		assertTrue(r.dirOnly());
+		assertTrue(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertTrue(r.getAttributes().isEmpty());
 		assertEquals(r.getPattern(), "/patter?");
 
 		r = new AttributesRule("patt*", "");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertTrue(r.getAttributes().isEmpty());
 		assertEquals(r.getPattern(), "patt*");
 
 		r = new AttributesRule("pattern", "attribute1");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertFalse(r.getAttributes().isEmpty());
 		assertEquals(r.getAttributes().size(), 1);
@@ -322,28 +322,28 @@
 
 		r = new AttributesRule("pattern", "attribute1 -attribute2");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertEquals(r.getAttributes().size(), 2);
 		assertEquals(r.getPattern(), "pattern");
 
 		r = new AttributesRule("pattern", "attribute1 \t-attribute2 \t");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertEquals(r.getAttributes().size(), 2);
 		assertEquals(r.getPattern(), "pattern");
 
 		r = new AttributesRule("pattern", "attribute1\t-attribute2\t");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertEquals(r.getAttributes().size(), 2);
 		assertEquals(r.getPattern(), "pattern");
 
 		r = new AttributesRule("pattern", "attribute1\t -attribute2\t ");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertEquals(r.getAttributes().size(), 2);
 		assertEquals(r.getPattern(), "pattern");
@@ -351,7 +351,7 @@
 		r = new AttributesRule("pattern",
 				"attribute1 -attribute2  attribute3=value ");
 		assertTrue(r.isNameOnly());
-		assertFalse(r.dirOnly());
+		assertFalse(r.isDirOnly());
 		assertNotNull(r.getAttributes());
 		assertEquals(r.getAttributes().size(), 3);
 		assertEquals(r.getPattern(), "pattern");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
index 0e595e6..7421e90 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
@@ -251,14 +251,17 @@
 	}
 
 	private void assertAttributesNode(String pathName,
-			AttributesNode attributesNode, List<Attribute> nodeAttrs) {
+			AttributesNode attributesNode, List<Attribute> nodeAttrs)
+					throws IOException {
 		if (attributesNode == null)
 			assertTrue(nodeAttrs == null || nodeAttrs.isEmpty());
 		else {
 
 			Attributes entryAttributes = new Attributes();
-			attributesNode.getAttributes(pathName,
-					false, entryAttributes);
+			new AttributesHandler(walk).mergeAttributes(attributesNode,
+					pathName,
+					false,
+					entryAttributes);
 
 			if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
 				for (Attribute attribute : nodeAttrs) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
index d478a7c..ec2370e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
@@ -50,6 +50,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.After;
 import org.junit.Test;
 
@@ -57,6 +60,8 @@
  * Test {@link AttributesNode}
  */
 public class AttributesNodeTest {
+	private static final TreeWalk DUMMY_WALK = new TreeWalk(
+			new InMemoryRepository(new DfsRepositoryDescription("FooBar")));
 
 	private static final Attribute A_SET_ATTR = new Attribute("A", SET);
 
@@ -162,9 +167,10 @@
 	}
 
 	private void assertAttribute(String path, AttributesNode node,
-			Attributes attrs) {
+			Attributes attrs) throws IOException {
 		Attributes attributes = new Attributes();
-		node.getAttributes(path, false, attributes);
+		new AttributesHandler(DUMMY_WALK).mergeAttributes(node, path, false,
+				attributes);
 		assertEquals(attrs, attributes);
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
index 4215ba2..b159cca 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
@@ -219,14 +219,16 @@
 	}
 
 	private void assertAttributesNode(String pathName,
-			AttributesNode attributesNode, List<Attribute> nodeAttrs) {
+			AttributesNode attributesNode, List<Attribute> nodeAttrs)
+					throws IOException {
 		if (attributesNode == null)
 			assertTrue(nodeAttrs == null || nodeAttrs.isEmpty());
 		else {
 
 			Attributes entryAttributes = new Attributes();
-			attributesNode.getAttributes(pathName,
-					false, entryAttributes);
+			new AttributesHandler(walk).mergeAttributes(attributesNode,
+					pathName, false,
+					entryAttributes);
 
 			if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
 				for (Attribute attribute : nodeAttrs) {
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
new file mode 100644
index 0000000..a5000dd
--- /dev/null
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit" version="2">
+    <resource path="src/org/eclipse/jgit/attributes/AttributesNode.java" type="org.eclipse.jgit.attributes.AttributesNode">
+        <filter comment="moved to new AttributesManager" id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.attributes.AttributesNode"/>
+                <message_argument value="getAttributes(String, boolean, Attributes)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/attributes/AttributesRule.java" type="org.eclipse.jgit.attributes.AttributesRule">
+        <filter comment="used only in tests: bean naming" id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.attributes.AttributesRule"/>
+                <message_argument value="dirOnly()"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java
new file mode 100644
index 0000000..19e4afd
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.attributes;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.attributes.Attribute.State;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
+
+/**
+ * The attributes handler knows how to retrieve, parse and merge attributes from
+ * the various gitattributes files. Furthermore it collects and expands macro
+ * expressions. The method {@link #getAttributes()} yields the ready processed
+ * attributes for the current path represented by the {@link TreeWalk}
+ * <p>
+ * The implementation is based on the specifications in
+ * http://git-scm.com/docs/gitattributes
+ *
+ * @since 4.3
+ */
+public class AttributesHandler {
+	private static final String MACRO_PREFIX = "[attr]"; //$NON-NLS-1$
+
+	private static final String BINARY_RULE_KEY = "binary"; //$NON-NLS-1$
+
+	/**
+	 * This is the default <b>binary</b> rule that is present in any git folder
+	 * <code>[attr]binary -diff -merge -text</code>
+	 */
+	private static final List<Attribute> BINARY_RULE_ATTRIBUTES = new AttributesRule(
+			MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$
+					.getAttributes();
+
+	private final TreeWalk treeWalk;
+
+	private final AttributesNode globalNode;
+
+	private final AttributesNode infoNode;
+
+	private final Map<String, List<Attribute>> expansions = new HashMap<>();
+
+	/**
+	 * Create an {@link AttributesHandler} with default rules as well as merged
+	 * rules from global, info and worktree root attributes
+	 *
+	 * @param treeWalk
+	 * @throws IOException
+	 */
+	public AttributesHandler(TreeWalk treeWalk) throws IOException {
+		this.treeWalk = treeWalk;
+		AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider();
+		this.globalNode = attributesNodeProvider != null
+				? attributesNodeProvider.getGlobalAttributesNode() : null;
+		this.infoNode = attributesNodeProvider != null
+				? attributesNodeProvider.getInfoAttributesNode() : null;
+
+		AttributesNode rootNode = attributesNode(treeWalk,
+				rootOf(
+						treeWalk.getTree(WorkingTreeIterator.class)),
+				rootOf(
+						treeWalk.getTree(DirCacheIterator.class)),
+				rootOf(treeWalk
+						.getTree(CanonicalTreeParser.class)));
+
+		expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES);
+		for (AttributesNode node : new AttributesNode[] { globalNode, rootNode,
+				infoNode }) {
+			if (node == null) {
+				continue;
+			}
+			for (AttributesRule rule : node.getRules()) {
+				if (rule.getPattern().startsWith(MACRO_PREFIX)) {
+					expansions.put(rule.getPattern()
+							.substring(MACRO_PREFIX.length()).trim(),
+							rule.getAttributes());
+				}
+			}
+		}
+	}
+
+	/**
+	 * see {@link TreeWalk#getAttributes()}
+	 *
+	 * @return the {@link Attributes} for the current path represented by the
+	 *         {@link TreeWalk}
+	 * @throws IOException
+	 */
+	public Attributes getAttributes() throws IOException {
+		String entryPath = treeWalk.getPathString();
+		boolean isDirectory = (treeWalk.getFileMode() == FileMode.TREE);
+		Attributes attributes = new Attributes();
+
+		// Gets the info attributes
+		mergeInfoAttributes(entryPath, isDirectory, attributes);
+
+		// Gets the attributes located on the current entry path
+		mergePerDirectoryEntryAttributes(entryPath, isDirectory,
+				treeWalk.getTree(WorkingTreeIterator.class),
+				treeWalk.getTree(DirCacheIterator.class),
+				treeWalk.getTree(CanonicalTreeParser.class),
+				attributes);
+
+		// Gets the attributes located in the global attribute file
+		mergeGlobalAttributes(entryPath, isDirectory, attributes);
+
+		// now after all attributes are collected - in the correct hierarchy
+		// order - remove all unspecified entries (the ! marker)
+		for (Attribute a : attributes.getAll()) {
+			if (a.getState() == State.UNSPECIFIED)
+				attributes.remove(a.getKey());
+		}
+
+		return attributes;
+	}
+
+	/**
+	 * Merges the matching GLOBAL attributes for an entry path.
+	 *
+	 * @param entryPath
+	 *            the path to test. The path must be relative to this attribute
+	 *            node's own repository path, and in repository path format
+	 *            (uses '/' and not '\').
+	 * @param isDirectory
+	 *            true if the target item is a directory.
+	 * @param result
+	 *            that will hold the attributes matching this entry path. This
+	 *            method will NOT override any existing entry in attributes.
+	 */
+	private void mergeGlobalAttributes(String entryPath, boolean isDirectory,
+			Attributes result) {
+		mergeAttributes(globalNode, entryPath, isDirectory, result);
+	}
+
+	/**
+	 * Merges the matching INFO attributes for an entry path.
+	 *
+	 * @param entryPath
+	 *            the path to test. The path must be relative to this attribute
+	 *            node's own repository path, and in repository path format
+	 *            (uses '/' and not '\').
+	 * @param isDirectory
+	 *            true if the target item is a directory.
+	 * @param result
+	 *            that will hold the attributes matching this entry path. This
+	 *            method will NOT override any existing entry in attributes.
+	 */
+	private void mergeInfoAttributes(String entryPath, boolean isDirectory,
+			Attributes result) {
+		mergeAttributes(infoNode, entryPath, isDirectory, result);
+	}
+
+	/**
+	 * Merges the matching working directory attributes for an entry path.
+	 *
+	 * @param entryPath
+	 *            the path to test. The path must be relative to this attribute
+	 *            node's own repository path, and in repository path format
+	 *            (uses '/' and not '\').
+	 * @param isDirectory
+	 *            true if the target item is a directory.
+	 * @param workingTreeIterator
+	 * @param dirCacheIterator
+	 * @param otherTree
+	 * @param result
+	 *            that will hold the attributes matching this entry path. This
+	 *            method will NOT override any existing entry in attributes.
+	 * @throws IOException
+	 */
+	private void mergePerDirectoryEntryAttributes(String entryPath,
+			boolean isDirectory,
+			@Nullable WorkingTreeIterator workingTreeIterator,
+			@Nullable DirCacheIterator dirCacheIterator,
+			@Nullable CanonicalTreeParser otherTree, Attributes result)
+					throws IOException {
+		// Prevents infinite recurrence
+		if (workingTreeIterator != null || dirCacheIterator != null
+				|| otherTree != null) {
+			AttributesNode attributesNode = attributesNode(
+					treeWalk, workingTreeIterator, dirCacheIterator, otherTree);
+			if (attributesNode != null) {
+				mergeAttributes(attributesNode, entryPath, isDirectory, result);
+			}
+			mergePerDirectoryEntryAttributes(entryPath, isDirectory,
+					parentOf(workingTreeIterator), parentOf(dirCacheIterator),
+					parentOf(otherTree), result);
+		}
+	}
+
+	/**
+	 * Merges the matching node attributes for an entry path.
+	 *
+	 * @param node
+	 *            the node to scan for matches to entryPath
+	 * @param entryPath
+	 *            the path to test. The path must be relative to this attribute
+	 *            node's own repository path, and in repository path format
+	 *            (uses '/' and not '\').
+	 * @param isDirectory
+	 *            true if the target item is a directory.
+	 * @param result
+	 *            that will hold the attributes matching this entry path. This
+	 *            method will NOT override any existing entry in attributes.
+	 */
+	protected void mergeAttributes(@Nullable AttributesNode node,
+			String entryPath,
+			boolean isDirectory, Attributes result) {
+		if (node == null)
+			return;
+		List<AttributesRule> rules = node.getRules();
+		// Parse rules in the reverse order that they were read since the last
+		// entry should be used
+		ListIterator<AttributesRule> ruleIterator = rules
+				.listIterator(rules.size());
+		while (ruleIterator.hasPrevious()) {
+			AttributesRule rule = ruleIterator.previous();
+			if (rule.isMatch(entryPath, isDirectory)) {
+				ListIterator<Attribute> attributeIte = rule.getAttributes()
+						.listIterator(rule.getAttributes().size());
+				// Parses the attributes in the reverse order that they were
+				// read since the last entry should be used
+				while (attributeIte.hasPrevious()) {
+					expandMacro(attributeIte.previous(), result);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @param attr
+	 * @param result
+	 *            contains the (recursive) expanded and merged macro attributes
+	 *            including the attribute iself
+	 */
+	protected void expandMacro(Attribute attr, Attributes result) {
+		// loop detection = exists check
+		if (result.containsKey(attr.getKey()))
+			return;
+
+		// also add macro to result set, same does native git
+		result.put(attr);
+
+		List<Attribute> expansion = expansions.get(attr.getKey());
+		if (expansion == null) {
+			return;
+		}
+		switch (attr.getState()) {
+		case UNSET: {
+			for (Attribute e : expansion) {
+				switch (e.getState()) {
+				case SET:
+					expandMacro(new Attribute(e.getKey(), State.UNSET), result);
+					break;
+				case UNSET:
+					expandMacro(new Attribute(e.getKey(), State.SET), result);
+					break;
+				case UNSPECIFIED:
+					expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
+							result);
+					break;
+				case CUSTOM:
+				default:
+					expandMacro(e, result);
+				}
+			}
+			break;
+		}
+		case CUSTOM: {
+			for (Attribute e : expansion) {
+				switch (e.getState()) {
+				case SET:
+				case UNSET:
+				case UNSPECIFIED:
+					expandMacro(e, result);
+					break;
+				case CUSTOM:
+				default:
+					expandMacro(new Attribute(e.getKey(), attr.getValue()),
+							result);
+				}
+			}
+			break;
+		}
+		case UNSPECIFIED: {
+			for (Attribute e : expansion) {
+				expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
+						result);
+			}
+			break;
+		}
+		case SET:
+		default:
+			for (Attribute e : expansion) {
+				expandMacro(e, result);
+			}
+			break;
+		}
+	}
+
+	/**
+	 * Get the {@link AttributesNode} for the current entry.
+	 * <p>
+	 * This method implements the fallback mechanism between the index and the
+	 * working tree depending on the operation type
+	 * </p>
+	 *
+	 * @param treeWalk
+	 * @param workingTreeIterator
+	 * @param dirCacheIterator
+	 * @param otherTree
+	 * @return a {@link AttributesNode} of the current entry,
+	 *         {@link NullPointerException} otherwise.
+	 * @throws IOException
+	 *             It raises an {@link IOException} if a problem appears while
+	 *             parsing one on the attributes file.
+	 */
+	private static AttributesNode attributesNode(TreeWalk treeWalk,
+			@Nullable WorkingTreeIterator workingTreeIterator,
+			@Nullable DirCacheIterator dirCacheIterator,
+			@Nullable CanonicalTreeParser otherTree) throws IOException {
+		AttributesNode attributesNode = null;
+		switch (treeWalk.getOperationType()) {
+		case CHECKIN_OP:
+			if (workingTreeIterator != null) {
+				attributesNode = workingTreeIterator.getEntryAttributesNode();
+			}
+			if (attributesNode == null && dirCacheIterator != null) {
+				attributesNode = dirCacheIterator
+						.getEntryAttributesNode(treeWalk.getObjectReader());
+			}
+			if (attributesNode == null && otherTree != null) {
+				attributesNode = otherTree
+						.getEntryAttributesNode(treeWalk.getObjectReader());
+			}
+			break;
+		case CHECKOUT_OP:
+			if (otherTree != null) {
+				attributesNode = otherTree
+						.getEntryAttributesNode(treeWalk.getObjectReader());
+			}
+			if (attributesNode == null && dirCacheIterator != null) {
+				attributesNode = dirCacheIterator
+						.getEntryAttributesNode(treeWalk.getObjectReader());
+			}
+			if (attributesNode == null && workingTreeIterator != null) {
+				attributesNode = workingTreeIterator.getEntryAttributesNode();
+			}
+			break;
+		default:
+			throw new IllegalStateException(
+					"The only supported operation types are:" //$NON-NLS-1$
+							+ OperationType.CHECKIN_OP + "," //$NON-NLS-1$
+							+ OperationType.CHECKOUT_OP);
+		}
+
+		return attributesNode;
+	}
+
+	private static <T extends AbstractTreeIterator> T parentOf(@Nullable T node) {
+		if(node==null) return null;
+		@SuppressWarnings("unchecked")
+		Class<T> type = (Class<T>) node.getClass();
+		AbstractTreeIterator parent = node.parent;
+		if (type.isInstance(parent)) {
+			return type.cast(parent);
+		}
+		return null;
+	}
+
+	private static <T extends AbstractTreeIterator> T rootOf(
+			@Nullable T node) {
+		if(node==null) return null;
+		AbstractTreeIterator t=node;
+		while (t!= null && t.parent != null) {
+			t= t.parent;
+		}
+		@SuppressWarnings("unchecked")
+		Class<T> type = (Class<T>) node.getClass();
+		if (type.isInstance(t)) {
+			return type.cast(t);
+		}
+		return null;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java
index 5c0aba2..7196502 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java
@@ -49,7 +49,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.ListIterator;
 
 import org.eclipse.jgit.lib.Constants;
 
@@ -122,40 +121,4 @@
 		return Collections.unmodifiableList(rules);
 	}
 
-	/**
-	 * Returns the matching attributes for an entry path.
-	 *
-	 * @param entryPath
-	 *            the path to test. The path must be relative to this attribute
-	 *            node's own repository path, and in repository path format
-	 *            (uses '/' and not '\').
-	 * @param isDirectory
-	 *            true if the target item is a directory.
-	 * @param attributes
-	 *            Map that will hold the attributes matching this entry path. If
-	 *            it is not empty, this method will NOT override any existing
-	 *            entry.
-	 * @since 4.2
-	 */
-	public void getAttributes(String entryPath,
-			boolean isDirectory, Attributes attributes) {
-		// Parse rules in the reverse order that they were read since the last
-		// entry should be used
-		ListIterator<AttributesRule> ruleIterator = rules.listIterator(rules
-				.size());
-		while (ruleIterator.hasPrevious()) {
-			AttributesRule rule = ruleIterator.previous();
-			if (rule.isMatch(entryPath, isDirectory)) {
-				ListIterator<Attribute> attributeIte = rule.getAttributes()
-						.listIterator(rule.getAttributes().size());
-				// Parses the attributes in the reverse order that they were
-				// read since the last entry should be used
-				while (attributeIte.hasPrevious()) {
-					Attribute attr = attributeIte.previous();
-					if (!attributes.containsKey(attr.getKey()))
-						attributes.put(attr);
-				}
-			}
-		}
-	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
index 35d18c4..0532250 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
@@ -109,10 +109,11 @@
 	private final String pattern;
 	private final List<Attribute> attributes;
 
-	private boolean nameOnly;
-	private boolean dirOnly;
+	private final boolean nameOnly;
 
-	private IMatcher matcher;
+	private final boolean dirOnly;
+
+	private final IMatcher matcher;
 
 	/**
 	 * Create a new attribute rule with the given pattern. Assumes that the
@@ -128,38 +129,43 @@
 	 */
 	public AttributesRule(String pattern, String attributes) {
 		this.attributes = parseAttributes(attributes);
-		nameOnly = false;
-		dirOnly = false;
 
 		if (pattern.endsWith("/")) { //$NON-NLS-1$
 			pattern = pattern.substring(0, pattern.length() - 1);
 			dirOnly = true;
+		} else {
+			dirOnly = false;
 		}
 
-		boolean hasSlash = pattern.contains("/"); //$NON-NLS-1$
+		int slashIndex = pattern.indexOf('/');
 
-		if (!hasSlash)
+		if (slashIndex < 0) {
 			nameOnly = true;
-		else if (!pattern.startsWith("/")) { //$NON-NLS-1$
+		} else if (slashIndex == 0) {
+			nameOnly = false;
+		} else {
+			nameOnly = false;
 			// Contains "/" but does not start with one
 			// Adding / to the start should not interfere with matching
 			pattern = "/" + pattern; //$NON-NLS-1$
 		}
 
+		IMatcher candidateMatcher = NO_MATCH;
 		try {
-			matcher = PathMatcher.createPathMatcher(pattern,
+			candidateMatcher = PathMatcher.createPathMatcher(pattern,
 					Character.valueOf(FastIgnoreRule.PATH_SEPARATOR), dirOnly);
 		} catch (InvalidPatternException e) {
-			matcher = NO_MATCH;
+			// ignore: invalid patterns are silently ignored
 		}
-
+		this.matcher = candidateMatcher;
 		this.pattern = pattern;
 	}
 
 	/**
 	 * @return True if the pattern should match directories only
+	 * @since 4.3
 	 */
-	public boolean dirOnly() {
+	public boolean isDirOnly() {
 		return dirOnly;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
index 5813635..dc835e4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
@@ -50,6 +50,7 @@
 import java.nio.CharBuffer;
 
 import org.eclipse.jgit.attributes.AttributesNode;
+import org.eclipse.jgit.attributes.AttributesHandler;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -88,8 +89,14 @@
 	/** A dummy object id buffer that matches the zero ObjectId. */
 	protected static final byte[] zeroid = new byte[Constants.OBJECT_ID_LENGTH];
 
-	/** Iterator for the parent tree; null if we are the root iterator. */
-	final AbstractTreeIterator parent;
+	/**
+	 * Iterator for the parent tree; null if we are the root iterator.
+	 * <p>
+	 * Used by {@link TreeWalk} and {@link AttributesHandler}
+	 *
+	 * @since 4.3
+	 */
+	public final AbstractTreeIterator parent;
 
 	/** The iterator this current entry is path equal to. */
 	AbstractTreeIterator matches;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
index 5cd713d..4775e96 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
@@ -49,15 +49,13 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.attributes.Attribute;
-import org.eclipse.jgit.attributes.Attribute.State;
 import org.eclipse.jgit.attributes.Attributes;
-import org.eclipse.jgit.attributes.AttributesNode;
 import org.eclipse.jgit.attributes.AttributesNodeProvider;
 import org.eclipse.jgit.attributes.AttributesProvider;
 import org.eclipse.jgit.dircache.DirCacheBuildIterator;
+import org.eclipse.jgit.attributes.AttributesHandler;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -270,6 +268,9 @@
 	/** Cached attribute for the current entry */
 	private Attributes attrs = null;
 
+	/** Cached attributes handler */
+	private AttributesHandler attributesHandler;
+
 	private Config config;
 
 	/**
@@ -310,6 +311,14 @@
 	}
 
 	/**
+	 * @return the {@link OperationType}
+	 * @since 4.3
+	 */
+	public OperationType getOperationType() {
+		return operationType;
+	}
+
+	/**
 	 * Release any resources used by this walker's reader.
 	 * <p>
 	 * A walker that has been released can be used again, but may need to be
@@ -435,9 +444,83 @@
 		attributesNodeProvider = provider;
 	}
 
+	/**
+	 * @return the {@link AttributesNodeProvider} for this {@link TreeWalk}.
+	 * @since 4.3
+	 */
+	public AttributesNodeProvider getAttributesNodeProvider() {
+		return attributesNodeProvider;
+	}
+
+	/**
+	 * Retrieve the git attributes for the current entry.
+	 *
+	 * <h4>Git attribute computation</h4>
+	 *
+	 * <ul>
+	 * <li>Get the attributes matching the current path entry from the info file
+	 * (see {@link AttributesNodeProvider#getInfoAttributesNode()}).</li>
+	 * <li>Completes the list of attributes using the .gitattributes files
+	 * located on the current path (the further the directory that contains
+	 * .gitattributes is from the path in question, the lower its precedence).
+	 * For a checkin operation, it will look first on the working tree (if any).
+	 * If there is no attributes file, it will fallback on the index. For a
+	 * checkout operation, it will first use the index entry and then fallback
+	 * on the working tree if none.</li>
+	 * <li>In the end, completes the list of matching attributes using the
+	 * global attribute file define in the configuration (see
+	 * {@link AttributesNodeProvider#getGlobalAttributesNode()})</li>
+	 *
+	 * </ul>
+	 *
+	 *
+	 * <h4>Iterator constraints</h4>
+	 *
+	 * <p>
+	 * In order to have a correct list of attributes for the current entry, this
+	 * {@link TreeWalk} requires to have at least one
+	 * {@link AttributesNodeProvider} and a {@link DirCacheIterator} set up. An
+	 * {@link AttributesNodeProvider} is used to retrieve the attributes from
+	 * the info attributes file and the global attributes file. The
+	 * {@link DirCacheIterator} is used to retrieve the .gitattributes files
+	 * stored in the index. A {@link WorkingTreeIterator} can also be provided
+	 * to access the local version of the .gitattributes files. If none is
+	 * provided it will fallback on the {@link DirCacheIterator}.
+	 * </p>
+	 *
+	 * @return a {@link Set} of {@link Attribute}s that match the current entry.
+	 * @since 4.2
+	 */
+	public Attributes getAttributes() {
+		if (attrs != null)
+			return attrs;
+
+		if (attributesNodeProvider == null) {
+			// The work tree should have a AttributesNodeProvider to be able to
+			// retrieve the info and global attributes node
+			throw new IllegalStateException(
+					"The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$
+		}
+
+		try {
+			// Lazy create the attributesHandler on the first access of
+			// attributes. This requires the info, global and root
+			// attributes nodes
+			if (attributesHandler == null) {
+				attributesHandler = new AttributesHandler(this);
+			}
+			attrs = attributesHandler.getAttributes();
+			return attrs;
+		} catch (IOException e) {
+			throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$
+					e);
+		}
+	}
+
 	/** Reset this walker so new tree iterators can be added to it. */
 	public void reset() {
 		attrs = null;
+		attributesHandler = null;
 		trees = NO_TREES;
 		advance = false;
 		depth = 0;
@@ -740,6 +823,16 @@
 	}
 
 	/**
+	 * Obtain the {@link FileMode} for the current entry on the currentHead tree
+	 *
+	 * @return mode for the current entry of the currentHead tree.
+	 * @since 4.3
+	 */
+	public FileMode getFileMode() {
+		return FileMode.fromBits(currentHead.mode);
+	}
+
+	/**
 	 * Obtain the ObjectId for the current entry.
 	 * <p>
 	 * Using this method to compare ObjectId values between trees of this walker
@@ -1109,156 +1202,13 @@
 	}
 
 	/**
-	 * Retrieve the git attributes for the current entry.
-	 *
-	 * <h4>Git attribute computation</h4>
-	 *
-	 * <ul>
-	 * <li>Get the attributes matching the current path entry from the info file
-	 * (see {@link AttributesNodeProvider#getInfoAttributesNode()}).</li>
-	 * <li>Completes the list of attributes using the .gitattributes files
-	 * located on the current path (the further the directory that contains
-	 * .gitattributes is from the path in question, the lower its precedence).
-	 * For a checkin operation, it will look first on the working tree (if any).
-	 * If there is no attributes file, it will fallback on the index. For a
-	 * checkout operation, it will first use the index entry and then fallback
-	 * on the working tree if none.</li>
-	 * <li>In the end, completes the list of matching attributes using the
-	 * global attribute file define in the configuration (see
-	 * {@link AttributesNodeProvider#getGlobalAttributesNode()})</li>
-	 *
-	 * </ul>
-	 *
-	 *
-	 * <h4>Iterator constraints</h4>
-	 *
-	 * <p>
-	 * In order to have a correct list of attributes for the current entry, this
-	 * {@link TreeWalk} requires to have at least one
-	 * {@link AttributesNodeProvider} and a {@link DirCacheIterator} set up. An
-	 * {@link AttributesNodeProvider} is used to retrieve the attributes from
-	 * the info attributes file and the global attributes file. The
-	 * {@link DirCacheIterator} is used to retrieve the .gitattributes files
-	 * stored in the index. A {@link WorkingTreeIterator} can also be provided
-	 * to access the local version of the .gitattributes files. If none is
-	 * provided it will fallback on the {@link DirCacheIterator}.
-	 * </p>
-	 *
-	 * @return a {@link Set} of {@link Attribute}s that match the current entry.
-	 * @since 4.2
+	 * @param type
+	 *            of the tree to be queried
+	 * @return the tree of that type or null if none is present
+	 * @since 4.3
 	 */
-	public Attributes getAttributes() {
-		if (attrs != null)
-			return attrs;
-
-		if (attributesNodeProvider == null) {
-			// The work tree should have a AttributesNodeProvider to be able to
-			// retrieve the info and global attributes node
-			throw new IllegalStateException(
-					"The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$
-		}
-
-		WorkingTreeIterator workingTreeIterator = getTree(WorkingTreeIterator.class);
-		DirCacheIterator dirCacheIterator = getTree(DirCacheIterator.class);
-		CanonicalTreeParser other = getTree(CanonicalTreeParser.class);
-
-		if (workingTreeIterator == null && dirCacheIterator == null
-				&& other == null) {
-			// Can not retrieve the attributes without at least one of the above
-			// iterators.
-			return new Attributes();
-		}
-
-		String path = currentHead.getEntryPathString();
-		final boolean isDir = FileMode.TREE.equals(currentHead.mode);
-		Attributes attributes = new Attributes();
-		try {
-			// Gets the global attributes node
-			AttributesNode globalNodeAttr = attributesNodeProvider
-					.getGlobalAttributesNode();
-			// Gets the info attributes node
-			AttributesNode infoNodeAttr = attributesNodeProvider
-					.getInfoAttributesNode();
-
-			// Gets the info attributes
-			if (infoNodeAttr != null) {
-				infoNodeAttr.getAttributes(path, isDir, attributes);
-			}
-
-			// Gets the attributes located on the current entry path
-			getPerDirectoryEntryAttributes(path, isDir, operationType,
-					workingTreeIterator, dirCacheIterator, other, attributes);
-
-			// Gets the attributes located in the global attribute file
-			if (globalNodeAttr != null) {
-				globalNodeAttr.getAttributes(path, isDir, attributes);
-			}
-		} catch (IOException e) {
-			throw new JGitInternalException("Error while parsing attributes", e); //$NON-NLS-1$
-		}
-		// now after all attributes are collected - in the correct hierarchy
-		// order - remove all unspecified entries (the ! marker)
-		for (Attribute a : attributes.getAll()) {
-			if (a.getState() == State.UNSPECIFIED)
-				attributes.remove(a.getKey());
-		}
-		return attributes;
-	}
-
-	/**
-	 * Get the attributes located on the current entry path.
-	 *
-	 * @param path
-	 *            current entry path
-	 * @param isDir
-	 *            holds true if the current entry is a directory
-	 * @param opType
-	 *            type of operation
-	 * @param workingTreeIterator
-	 *            a {@link WorkingTreeIterator} matching the current entry
-	 * @param dirCacheIterator
-	 *            a {@link DirCacheIterator} matching the current entry
-	 * @param other
-	 *            a {@link CanonicalTreeParser} matching the current entry
-	 * @param attributes
-	 *            Non null map holding the existing attributes. This map will be
-	 *            augmented with new entry. None entry will be overrided.
-	 * @throws IOException
-	 *             It raises an {@link IOException} if a problem appears while
-	 *             parsing one on the attributes file.
-	 */
-	private void getPerDirectoryEntryAttributes(String path, boolean isDir,
-			OperationType opType, WorkingTreeIterator workingTreeIterator,
-			DirCacheIterator dirCacheIterator, CanonicalTreeParser other,
-			Attributes attributes)
-			throws IOException {
-		// Prevents infinite recurrence
-		if (workingTreeIterator != null || dirCacheIterator != null
-				|| other != null) {
-			AttributesNode currentAttributesNode = getCurrentAttributesNode(
-					opType, workingTreeIterator, dirCacheIterator, other);
-			if (currentAttributesNode != null) {
-				currentAttributesNode.getAttributes(path, isDir, attributes);
-			}
-			getPerDirectoryEntryAttributes(path, isDir, opType,
-					getParent(workingTreeIterator, WorkingTreeIterator.class),
-					getParent(dirCacheIterator, DirCacheIterator.class),
-					getParent(other, CanonicalTreeParser.class), attributes);
-		}
-	}
-
-	private static <T extends AbstractTreeIterator> T getParent(T current,
+	public <T extends AbstractTreeIterator> T getTree(
 			Class<T> type) {
-		if (current != null) {
-			AbstractTreeIterator parent = current.parent;
-			if (type.isInstance(parent)) {
-				return type.cast(parent);
-			}
-		}
-		return null;
-	}
-
-	private <T extends AbstractTreeIterator> T getTree(Class<T> type) {
 		for (int i = 0; i < trees.length; i++) {
 			AbstractTreeIterator tree = trees[i];
 			if (type.isInstance(tree)) {
@@ -1269,76 +1219,6 @@
 	}
 
 	/**
-	 * Get the {@link AttributesNode} for the current entry.
-	 * <p>
-	 * This method implements the fallback mechanism between the index and the
-	 * working tree depending on the operation type
-	 * </p>
-	 *
-	 * @param opType
-	 * @param workingTreeIterator
-	 * @param dirCacheIterator
-	 * @param other
-	 * @return a {@link AttributesNode} of the current entry,
-	 *         {@link NullPointerException} otherwise.
-	 * @throws IOException
-	 *             It raises an {@link IOException} if a problem appears while
-	 *             parsing one on the attributes file.
-	 */
-	private AttributesNode getCurrentAttributesNode(OperationType opType,
-			@Nullable WorkingTreeIterator workingTreeIterator,
-			@Nullable DirCacheIterator dirCacheIterator,
-			@Nullable CanonicalTreeParser other)
-					throws IOException {
-		AttributesNode attributesNode = null;
-		switch (opType) {
-		case CHECKIN_OP:
-			if (workingTreeIterator != null) {
-				attributesNode = workingTreeIterator.getEntryAttributesNode();
-			}
-			if (attributesNode == null && dirCacheIterator != null) {
-				attributesNode = getAttributesNode(dirCacheIterator
-						.getEntryAttributesNode(getObjectReader()),
-						attributesNode);
-			}
-			if (attributesNode == null && other != null) {
-				attributesNode = getAttributesNode(
-						other.getEntryAttributesNode(getObjectReader()),
-						attributesNode);
-			}
-			break;
-		case CHECKOUT_OP:
-			if (other != null) {
-				attributesNode = other
-						.getEntryAttributesNode(getObjectReader());
-			}
-			if (dirCacheIterator != null) {
-				attributesNode = getAttributesNode(dirCacheIterator
-						.getEntryAttributesNode(getObjectReader()),
-						attributesNode);
-			}
-			if (attributesNode == null && workingTreeIterator != null) {
-				attributesNode = getAttributesNode(
-						workingTreeIterator.getEntryAttributesNode(),
-						attributesNode);
-			}
-			break;
-		default:
-			throw new IllegalStateException(
-					"The only supported operation types are:" //$NON-NLS-1$
-							+ OperationType.CHECKIN_OP + "," //$NON-NLS-1$
-							+ OperationType.CHECKOUT_OP);
-		}
-
-		return attributesNode;
-	}
-
-	private static AttributesNode getAttributesNode(AttributesNode value,
-			AttributesNode defaultValue) {
-		return (value == null) ? defaultValue : value;
-	}
-
-	/**
 	 * Inspect config and attributes to return a filtercommand applicable for
 	 * the current path
 	 *