]> source.dussan.org Git - jgit.git/commitdiff
Add basic support for .gitattributes 14/1614/24
authorArthur Daussy <arthur.daussy@obeo.fr>
Thu, 31 Jan 2013 19:27:10 +0000 (20:27 +0100)
committerChris Aniszczyk <caniszczyk@gmail.com>
Wed, 7 Jan 2015 18:52:06 +0000 (10:52 -0800)
Core classes to parse and process .gitattributes files including
support for reading attributes in WorkingTreeIterator and the
dirCacheIterator.

The implementation follows the git ignore implementation. It supports
lazy reading attributes while walking the working tree.

Bug: 342372
CQ: 9078
Change-Id: I05f3ce1861fbf9896b1bcb7816ba78af35f3ad3d
Also-by: Marc Strapetz <marc.strapetz@syntevo.com>
Also-by: Gunnar Wagenknecht <gunnar@wagenknecht.org>
Also-by: Arthur Daussy <arthur.daussy@obeo.fr>
Signed-off-by: Gunnar Wagenknecht <gunnar@wagenknecht.org>
Signed-off-by: Marc Strapetz <marc.strapetz@syntevo.com>
Signed-off-by: Arthur Daussy <arthur.daussy@obeo.fr>
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
18 files changed:
org.eclipse.jgit.test/META-INF/MANIFEST.MF
org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeNodeTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java [new file with mode: 0644]
org.eclipse.jgit/META-INF/MANIFEST.MF
org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/attributes/package-info.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java
org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/IMatcher.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java

index 6ca3df17a3a6c1d4bbe877fb124cfda71446111d..af40e883cd7133fba5957139867ac392094c82f6 100644 (file)
@@ -10,6 +10,7 @@ Bundle-RequiredExecutionEnvironment: J2SE-1.5
 Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)",
  org.eclipse.jgit.api;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.api.errors;version="[3.7.0,3.8.0)",
+ org.eclipse.jgit.attributes;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.awtui;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.blame;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.console;version="[3.7.0,3.8.0)",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeNodeTest.java
new file mode 100644 (file)
index 0000000..ea25036
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2014, Obeo.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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.eclipse.jgit.attributes.Attribute.State.SET;
+import static org.eclipse.jgit.attributes.Attribute.State.UNSET;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Test;
+
+/**
+ * Test {@link AttributesNode}
+ */
+public class AttributeNodeTest {
+
+       private static final Attribute A_SET_ATTR = new Attribute("A", SET);
+
+       private static final Attribute A_UNSET_ATTR = new Attribute("A", UNSET);
+
+       private static final Attribute B_SET_ATTR = new Attribute("B", SET);
+
+       private static final Attribute B_UNSET_ATTR = new Attribute("B", UNSET);
+
+       private static final Attribute C_VALUE_ATTR = new Attribute("C", "value");
+
+       private static final Attribute C_VALUE2_ATTR = new Attribute("C", "value2");
+
+       private InputStream is;
+
+       @After
+       public void after() throws IOException {
+               if (is != null)
+                       is.close();
+       }
+
+       @Test
+       public void testBasic() throws IOException {
+               String attributeFileContent = "*.type1 A -B C=value\n"
+                               + "*.type2 -A B C=value2";
+
+               is = new ByteArrayInputStream(attributeFileContent.getBytes());
+               AttributesNode node = new AttributesNode();
+               node.parse(is);
+               assertAttribute("file.type1", node,
+                               asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR));
+               assertAttribute("file.type2", node,
+                               asSet(A_UNSET_ATTR, B_SET_ATTR, C_VALUE2_ATTR));
+       }
+
+       @Test
+       public void testNegativePattern() throws IOException {
+               String attributeFileContent = "!*.type1 A -B C=value\n"
+                               + "!*.type2 -A B C=value2";
+
+               is = new ByteArrayInputStream(attributeFileContent.getBytes());
+               AttributesNode node = new AttributesNode();
+               node.parse(is);
+               assertAttribute("file.type1", node, Collections.<Attribute> emptySet());
+               assertAttribute("file.type2", node, Collections.<Attribute> emptySet());
+       }
+
+       @Test
+       public void testEmptyNegativeAttributeKey() throws IOException {
+               String attributeFileContent = "*.type1 - \n" //
+                               + "*.type2 -   -A";
+               is = new ByteArrayInputStream(attributeFileContent.getBytes());
+               AttributesNode node = new AttributesNode();
+               node.parse(is);
+               assertAttribute("file.type1", node, Collections.<Attribute> emptySet());
+               assertAttribute("file.type2", node, asSet(A_UNSET_ATTR));
+       }
+
+       @Test
+       public void testEmptyValueKey() throws IOException {
+               String attributeFileContent = "*.type1 = \n" //
+                               + "*.type2 =value\n"//
+                               + "*.type3 attr=\n";
+               is = new ByteArrayInputStream(attributeFileContent.getBytes());
+               AttributesNode node = new AttributesNode();
+               node.parse(is);
+               assertAttribute("file.type1", node, Collections.<Attribute> emptySet());
+               assertAttribute("file.type2", node, Collections.<Attribute> emptySet());
+               assertAttribute("file.type3", node, asSet(new Attribute("attr", "")));
+       }
+
+       @Test
+       public void testEmptyLine() throws IOException {
+               String attributeFileContent = "*.type1 A -B C=value\n" //
+                               + "\n" //
+                               + "    \n" //
+                               + "*.type2 -A B C=value2";
+
+               is = new ByteArrayInputStream(attributeFileContent.getBytes());
+               AttributesNode node = new AttributesNode();
+               node.parse(is);
+               assertAttribute("file.type1", node,
+                               asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR));
+               assertAttribute("file.type2", node,
+                               asSet(A_UNSET_ATTR, B_SET_ATTR, C_VALUE2_ATTR));
+       }
+
+       @Test
+       public void testTabSeparator() throws IOException {
+               String attributeFileContent = "*.type1 \tA -B\tC=value\n"
+                               + "*.type2\t -A\tB C=value2\n" //
+                               + "*.type3  \t\t   B\n" //
+                               + "*.type3\t-A";//
+
+               is = new ByteArrayInputStream(attributeFileContent.getBytes());
+               AttributesNode node = new AttributesNode();
+               node.parse(is);
+               assertAttribute("file.type1", node,
+                               asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR));
+               assertAttribute("file.type2", node,
+                               asSet(A_UNSET_ATTR, B_SET_ATTR, C_VALUE2_ATTR));
+               assertAttribute("file.type3", node, asSet(A_UNSET_ATTR, B_SET_ATTR));
+       }
+
+       private void assertAttribute(String path, AttributesNode node,
+                       Set<Attribute> attrs) {
+               HashMap<String, Attribute> attributes = new HashMap<String, Attribute>();
+               node.getAttributes(path, false, attributes);
+               assertEquals(attrs, new HashSet<Attribute>(attributes.values()));
+       }
+
+       static Set<Attribute> asSet(Attribute... attrs) {
+               Set<Attribute> result = new HashSet<Attribute>();
+               for (Attribute attr : attrs)
+                       result.add(attr);
+               return result;
+       }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java
new file mode 100644 (file)
index 0000000..93b954f
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010, Marc Strapetz <marc.strapetz@syntevo.com>
+ * Copyright (C) 2013, Gunnar Wagenknecht
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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.assertNull;
+
+import org.eclipse.jgit.attributes.Attribute.State;
+import org.junit.Test;
+
+/**
+ * Tests {@link Attribute}
+ */
+public class AttributeTest {
+
+       @Test
+       public void testBasic() {
+               Attribute a = new Attribute("delta", State.SET);
+               assertEquals(a.getKey(), "delta");
+               assertEquals(a.getState(), State.SET);
+               assertNull(a.getValue());
+               assertEquals(a.toString(), "delta");
+
+               a = new Attribute("delta", State.UNSET);
+               assertEquals(a.getKey(), "delta");
+               assertEquals(a.getState(), State.UNSET);
+               assertNull(a.getValue());
+               assertEquals(a.toString(), "-delta");
+
+               a = new Attribute("delta", "value");
+               assertEquals(a.getKey(), "delta");
+               assertEquals(a.getState(), State.CUSTOM);
+               assertEquals(a.getValue(), "value");
+               assertEquals(a.toString(), "delta=value");
+       }
+}
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
new file mode 100644 (file)
index 0000000..6865406
--- /dev/null
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Tests git attributes pattern matches
+ * <p>
+ * Inspired by {@link org.eclipse.jgit.ignore.IgnoreMatcherTest}
+ * </p>
+ */
+public class AttributesMatcherTest {
+
+       @Test
+       public void testBasic() {
+               String pattern = "/test.stp";
+               assertMatched(pattern, "/test.stp");
+
+               pattern = "#/test.stp";
+               assertNotMatched(pattern, "/test.stp");
+       }
+
+       @Test
+       public void testFileNameWildcards() {
+               //Test basic * and ? for any pattern + any character
+               String pattern = "*.st?";
+               assertMatched(pattern, "/test.stp");
+               assertMatched(pattern, "/anothertest.stg");
+               assertMatched(pattern, "/anothertest.st0");
+               assertNotMatched(pattern, "/anothertest.sta1");
+               //Check that asterisk does not expand to "/"
+               assertNotMatched(pattern, "/another/test.sta1");
+
+               //Same as above, with a leading slash to ensure that doesn't cause problems
+               pattern = "/*.st?";
+               assertMatched(pattern, "/test.stp");
+               assertMatched(pattern, "/anothertest.stg");
+               assertMatched(pattern, "/anothertest.st0");
+               assertNotMatched(pattern, "/anothertest.sta1");
+               //Check that asterisk does not expand to "/"
+               assertNotMatched(pattern, "/another/test.sta1");
+
+               //Test for numbers
+               pattern = "*.sta[0-5]";
+               assertMatched(pattern,  "/test.sta5");
+               assertMatched(pattern, "/test.sta4");
+               assertMatched(pattern, "/test.sta3");
+               assertMatched(pattern, "/test.sta2");
+               assertMatched(pattern, "/test.sta1");
+               assertMatched(pattern, "/test.sta0");
+               assertMatched(pattern, "/anothertest.sta2");
+               assertNotMatched(pattern, "test.stag");
+               assertNotMatched(pattern, "test.sta6");
+
+               //Test for letters
+               pattern = "/[tv]est.sta[a-d]";
+               assertMatched(pattern,  "/test.staa");
+               assertMatched(pattern, "/test.stab");
+               assertMatched(pattern, "/test.stac");
+               assertMatched(pattern, "/test.stad");
+               assertMatched(pattern, "/vest.stac");
+               assertNotMatched(pattern, "test.stae");
+               assertNotMatched(pattern, "test.sta9");
+
+               //Test child directory/file is matched
+               pattern = "/src/ne?";
+               assertMatched(pattern, "/src/new/");
+               assertMatched(pattern, "/src/new");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/src/new/a/a.c");
+               assertNotMatched(pattern, "/src/new.c");
+
+               //Test name-only fnmatcher matches
+               pattern = "ne?";
+               assertMatched(pattern, "/src/new/");
+               assertMatched(pattern, "/src/new");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/src/new/a/a.c");
+               assertMatched(pattern, "/neb");
+               assertNotMatched(pattern, "/src/new.c");
+       }
+
+       @Test
+       public void testTargetWithoutLeadingSlash() {
+               //Test basic * and ? for any pattern + any character
+               String pattern = "/*.st?";
+               assertMatched(pattern, "test.stp");
+               assertMatched(pattern, "anothertest.stg");
+               assertMatched(pattern, "anothertest.st0");
+               assertNotMatched(pattern, "anothertest.sta1");
+               //Check that asterisk does not expand to ""
+               assertNotMatched(pattern, "another/test.sta1");
+
+               //Same as above, with a leading slash to ensure that doesn't cause problems
+               pattern = "/*.st?";
+               assertMatched(pattern, "test.stp");
+               assertMatched(pattern, "anothertest.stg");
+               assertMatched(pattern, "anothertest.st0");
+               assertNotMatched(pattern, "anothertest.sta1");
+               //Check that asterisk does not expand to ""
+               assertNotMatched(pattern, "another/test.sta1");
+
+               //Test for numbers
+               pattern = "/*.sta[0-5]";
+               assertMatched(pattern,  "test.sta5");
+               assertMatched(pattern, "test.sta4");
+               assertMatched(pattern, "test.sta3");
+               assertMatched(pattern, "test.sta2");
+               assertMatched(pattern, "test.sta1");
+               assertMatched(pattern, "test.sta0");
+               assertMatched(pattern, "anothertest.sta2");
+               assertNotMatched(pattern, "test.stag");
+               assertNotMatched(pattern, "test.sta6");
+
+               //Test for letters
+               pattern = "/[tv]est.sta[a-d]";
+               assertMatched(pattern,  "test.staa");
+               assertMatched(pattern, "test.stab");
+               assertMatched(pattern, "test.stac");
+               assertMatched(pattern, "test.stad");
+               assertMatched(pattern, "vest.stac");
+               assertNotMatched(pattern, "test.stae");
+               assertNotMatched(pattern, "test.sta9");
+
+               //Test child directory/file is matched
+               pattern = "/src/ne?";
+               assertMatched(pattern, "src/new/");
+               assertMatched(pattern, "src/new");
+               assertMatched(pattern, "src/new/a.c");
+               assertMatched(pattern, "src/new/a/a.c");
+               assertNotMatched(pattern, "src/new.c");
+
+               //Test name-only fnmatcher matches
+               pattern = "ne?";
+               assertMatched(pattern, "src/new/");
+               assertMatched(pattern, "src/new");
+               assertMatched(pattern, "src/new/a.c");
+               assertMatched(pattern, "src/new/a/a.c");
+               assertMatched(pattern, "neb");
+               assertNotMatched(pattern, "src/new.c");
+       }
+
+       @Test
+       public void testParentDirectoryGitAttributes() {
+               //Contains git attribute patterns such as might be seen in a parent directory
+
+               //Test for wildcards
+               String pattern = "/*/*.c";
+               assertMatched(pattern, "/file/a.c");
+               assertMatched(pattern, "/src/a.c");
+               assertNotMatched(pattern, "/src/new/a.c");
+
+               //Test child directory/file is matched
+               pattern = "/src/new";
+               assertMatched(pattern, "/src/new/");
+               assertMatched(pattern, "/src/new");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/src/new/a/a.c");
+               assertNotMatched(pattern, "/src/new.c");
+
+               //Test child directory is matched, slash after name
+               pattern = "/src/new/";
+               assertMatched(pattern, "/src/new/");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/src/new/a/a.c");
+               assertNotMatched(pattern, "/src/new");
+               assertNotMatched(pattern, "/src/new.c");
+
+               //Test directory is matched by name only
+               pattern = "b1";
+               assertMatched(pattern, "/src/new/a/b1/a.c");
+               assertNotMatched(pattern, "/src/new/a/b2/file.c");
+               assertNotMatched(pattern, "/src/new/a/bb1/file.c");
+               assertNotMatched(pattern, "/src/new/a/file.c");
+       }
+
+       @Test
+       public void testTrailingSlash() {
+               String pattern = "/src/";
+               assertMatched(pattern, "/src/");
+               assertMatched(pattern, "/src/new");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/src/a.c");
+               assertNotMatched(pattern, "/src");
+               assertNotMatched(pattern, "/srcA/");
+       }
+
+       @Test
+       public void testNameOnlyMatches() {
+               /*
+                * Name-only matches do not contain any path separators
+                */
+               //Test matches for file extension
+               String pattern = "*.stp";
+               assertMatched(pattern, "/test.stp");
+               assertMatched(pattern, "/src/test.stp");
+               assertNotMatched(pattern, "/test.stp1");
+               assertNotMatched(pattern, "/test.astp");
+
+               //Test matches for name-only, applies to file name or folder name
+               pattern = "src";
+               assertMatched(pattern, "/src");
+               assertMatched(pattern, "/src/");
+               assertMatched(pattern, "/src/a.c");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/new/src/a.c");
+               assertMatched(pattern, "/file/src");
+
+               //Test matches for name-only, applies only to folder names
+               pattern = "src/";
+               assertMatched(pattern, "/src/");
+               assertMatched(pattern, "/src/a.c");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/new/src/a.c");
+               assertNotMatched(pattern, "/src");
+               assertNotMatched(pattern, "/file/src");
+
+               //Test matches for name-only, applies to file name or folder name
+               //With a small wildcard
+               pattern = "?rc";
+               assertMatched(pattern, "/src/a.c");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/new/src/a.c");
+               assertMatched(pattern, "/file/src");
+               assertMatched(pattern, "/src/");
+
+               //Test matches for name-only, applies to file name or folder name
+               //With a small wildcard
+               pattern = "?r[a-c]";
+               assertMatched(pattern, "/src/a.c");
+               assertMatched(pattern, "/src/new/a.c");
+               assertMatched(pattern, "/new/src/a.c");
+               assertMatched(pattern, "/file/src");
+               assertMatched(pattern, "/src/");
+               assertMatched(pattern, "/srb/a.c");
+               assertMatched(pattern, "/grb/new/a.c");
+               assertMatched(pattern, "/new/crb/a.c");
+               assertMatched(pattern, "/file/3rb");
+               assertMatched(pattern, "/xrb/");
+               assertMatched(pattern, "/3ra/a.c");
+               assertMatched(pattern, "/5ra/new/a.c");
+               assertMatched(pattern, "/new/1ra/a.c");
+               assertMatched(pattern, "/file/dra");
+               assertMatched(pattern, "/era/");
+               assertNotMatched(pattern, "/crg");
+               assertNotMatched(pattern, "/cr3");
+       }
+
+       @Test
+       public void testGetters() {
+               AttributesRule r = new AttributesRule("/pattern/", "");
+               assertFalse(r.isNameOnly());
+               assertTrue(r.dirOnly());
+               assertNotNull(r.getAttributes());
+               assertTrue(r.getAttributes().isEmpty());
+               assertEquals(r.getPattern(), "/pattern");
+
+               r = new AttributesRule("/patter?/", "");
+               assertFalse(r.isNameOnly());
+               assertTrue(r.dirOnly());
+               assertNotNull(r.getAttributes());
+               assertTrue(r.getAttributes().isEmpty());
+               assertEquals(r.getPattern(), "/patter?");
+
+               r = new AttributesRule("patt*", "");
+               assertTrue(r.isNameOnly());
+               assertFalse(r.dirOnly());
+               assertNotNull(r.getAttributes());
+               assertTrue(r.getAttributes().isEmpty());
+               assertEquals(r.getPattern(), "patt*");
+
+               r = new AttributesRule("pattern", "attribute1");
+               assertTrue(r.isNameOnly());
+               assertFalse(r.dirOnly());
+               assertNotNull(r.getAttributes());
+               assertFalse(r.getAttributes().isEmpty());
+               assertEquals(r.getAttributes().size(), 1);
+               assertEquals(r.getPattern(), "pattern");
+
+               r = new AttributesRule("pattern", "attribute1 -attribute2");
+               assertTrue(r.isNameOnly());
+               assertFalse(r.dirOnly());
+               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());
+               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());
+               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());
+               assertNotNull(r.getAttributes());
+               assertEquals(r.getAttributes().size(), 2);
+               assertEquals(r.getPattern(), "pattern");
+
+               r = new AttributesRule("pattern",
+                               "attribute1 -attribute2  attribute3=value ");
+               assertTrue(r.isNameOnly());
+               assertFalse(r.dirOnly());
+               assertNotNull(r.getAttributes());
+               assertEquals(r.getAttributes().size(), 3);
+               assertEquals(r.getPattern(), "pattern");
+               assertEquals(r.getAttributes().get(0).toString(), "attribute1");
+               assertEquals(r.getAttributes().get(1).toString(), "-attribute2");
+               assertEquals(r.getAttributes().get(2).toString(), "attribute3=value");
+       }
+
+       /**
+        * Check for a match. If target ends with "/", match will assume that the
+        * target is meant to be a directory.
+        *
+        * @param pattern
+        *            Pattern as it would appear in a .gitattributes file
+        * @param target
+        *            Target file path relative to repository's GIT_DIR
+        */
+       public void assertMatched(String pattern, String target) {
+               boolean value = match(pattern, target);
+               assertTrue("Expected a match for: " + pattern + " with: " + target,
+                               value);
+       }
+
+       /**
+        * Check for a match. If target ends with "/", match will assume that the
+        * target is meant to be a directory.
+        *
+        * @param pattern
+        *            Pattern as it would appear in a .gitattributes file
+        * @param target
+        *            Target file path relative to repository's GIT_DIR
+        */
+       public void assertNotMatched(String pattern, String target) {
+               boolean value = match(pattern, target);
+               assertFalse("Expected no match for: " + pattern + " with: " + target,
+                               value);
+       }
+
+       /**
+        * Check for a match. If target ends with "/", match will assume that the
+        * target is meant to be a directory.
+        *
+        * @param pattern
+        *            Pattern as it would appear in a .gitattributes file
+        * @param target
+        *            Target file path relative to repository's GIT_DIR
+        * @return Result of {@link AttributesRule#isMatch(String, boolean)}
+        */
+       private static boolean match(String pattern, String target) {
+               AttributesRule r = new AttributesRule(pattern, "");
+               //If speed of this test is ever an issue, we can use a presetRule field
+               //to avoid recompiling a pattern each time.
+               return r.isMatch(target, target.endsWith("/"));
+       }
+}
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
new file mode 100644 (file)
index 0000000..49279e6
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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 java.util.Arrays.asList;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.attributes.Attribute.State;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests attributes node behavior on the the index.
+ */
+public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase {
+
+       private static final FileMode D = FileMode.TREE;
+
+       private static final FileMode F = FileMode.REGULAR_FILE;
+
+       private static Attribute EOL_LF = new Attribute("eol", "lf");
+
+       private static Attribute DELTA_UNSET = new Attribute("delta", State.UNSET);
+
+       private Git git;
+
+       private TreeWalk walk;
+
+       @Override
+       @Before
+       public void setUp() throws Exception {
+               super.setUp();
+               git = new Git(db);
+
+       }
+
+       @Test
+       public void testRules() throws Exception {
+               writeAttributesFile(".git/info/attributes", "windows* eol=crlf");
+
+               writeAttributesFile(".gitattributes", "*.txt eol=lf");
+               writeTrashFile("windows.file", "");
+               writeTrashFile("windows.txt", "");
+               writeTrashFile("readme.txt", "");
+
+               writeAttributesFile("src/config/.gitattributes", "*.txt -delta");
+               writeTrashFile("src/config/readme.txt", "");
+               writeTrashFile("src/config/windows.file", "");
+               writeTrashFile("src/config/windows.txt", "");
+
+               // Adds file to index
+               git.add().addFilepattern(".").call();
+
+               walk = beginWalk();
+
+               assertIteration(F, ".gitattributes");
+               assertIteration(F, "readme.txt", asList(EOL_LF));
+
+               assertIteration(D, "src");
+
+               assertIteration(D, "src/config");
+               assertIteration(F, "src/config/.gitattributes");
+               assertIteration(F, "src/config/readme.txt", asList(DELTA_UNSET));
+               assertIteration(F, "src/config/windows.file", null);
+               assertIteration(F, "src/config/windows.txt", asList(DELTA_UNSET));
+
+               assertIteration(F, "windows.file", null);
+               assertIteration(F, "windows.txt", asList(EOL_LF));
+
+               endWalk();
+       }
+
+       /**
+        * Checks that if there is no .gitattributes file in the repository
+        * everything still work fine.
+        *
+        * @throws Exception
+        */
+       @Test
+       public void testNoAttributes() throws Exception {
+               writeTrashFile("l0.txt", "");
+               writeTrashFile("level1/l1.txt", "");
+               writeTrashFile("level1/level2/l2.txt", "");
+
+               // Adds file to index
+               git.add().addFilepattern(".").call();
+               walk = beginWalk();
+
+               assertIteration(F, "l0.txt");
+
+               assertIteration(D, "level1");
+               assertIteration(F, "level1/l1.txt");
+
+               assertIteration(D, "level1/level2");
+               assertIteration(F, "level1/level2/l2.txt");
+
+               endWalk();
+       }
+
+       /**
+        * Checks that empty .gitattribute files do not return incorrect value.
+        *
+        * @throws Exception
+        */
+       @Test
+       public void testEmptyGitAttributeFile() throws Exception {
+               writeAttributesFile(".git/info/attributes", "");
+               writeTrashFile("l0.txt", "");
+               writeAttributesFile(".gitattributes", "");
+               writeTrashFile("level1/l1.txt", "");
+               writeTrashFile("level1/level2/l2.txt", "");
+
+               // Adds file to index
+               git.add().addFilepattern(".").call();
+               walk = beginWalk();
+
+               assertIteration(F, ".gitattributes");
+               assertIteration(F, "l0.txt");
+
+               assertIteration(D, "level1");
+               assertIteration(F, "level1/l1.txt");
+
+               assertIteration(D, "level1/level2");
+               assertIteration(F, "level1/level2/l2.txt");
+
+               endWalk();
+       }
+
+       @Test
+       public void testNoMatchingAttributes() throws Exception {
+               writeAttributesFile(".git/info/attributes", "*.java delta");
+               writeAttributesFile(".gitattributes", "*.java -delta");
+               writeAttributesFile("levelA/.gitattributes", "*.java eol=lf");
+               writeAttributesFile("levelB/.gitattributes", "*.txt eol=lf");
+
+               writeTrashFile("levelA/lA.txt", "");
+
+               // Adds file to index
+               git.add().addFilepattern(".").call();
+               walk = beginWalk();
+
+               assertIteration(F, ".gitattributes");
+
+               assertIteration(D, "levelA");
+               assertIteration(F, "levelA/.gitattributes");
+               assertIteration(F, "levelA/lA.txt");
+
+               assertIteration(D, "levelB");
+               assertIteration(F, "levelB/.gitattributes");
+
+               endWalk();
+       }
+
+       @Test
+       public void testIncorrectAttributeFileName() throws Exception {
+               writeAttributesFile("levelA/file.gitattributes", "*.txt -delta");
+               writeAttributesFile("gitattributes", "*.txt eol=lf");
+
+               writeTrashFile("l0.txt", "");
+               writeTrashFile("levelA/lA.txt", "");
+
+               // Adds file to index
+               git.add().addFilepattern(".").call();
+               walk = beginWalk();
+
+               assertIteration(F, "gitattributes");
+
+               assertIteration(F, "l0.txt");
+
+               assertIteration(D, "levelA");
+               assertIteration(F, "levelA/file.gitattributes");
+               assertIteration(F, "levelA/lA.txt");
+
+               endWalk();
+       }
+
+       private void assertIteration(FileMode type, String pathName)
+                       throws IOException {
+               assertIteration(type, pathName, Collections.<Attribute> emptyList());
+       }
+
+       private void assertIteration(FileMode type, String pathName,
+                       List<Attribute> nodeAttrs) throws IOException {
+               assertTrue("walk has entry", walk.next());
+               assertEquals(pathName, walk.getPathString());
+               assertEquals(type, walk.getFileMode(0));
+               DirCacheIterator itr = walk.getTree(0, DirCacheIterator.class);
+               assertNotNull("has tree", itr);
+
+               AttributesNode attributeNode = itr.getEntryAttributesNode(db
+                               .newObjectReader());
+               assertAttributeNode(pathName, attributeNode, nodeAttrs);
+
+               if (D.equals(type))
+                       walk.enterSubtree();
+
+       }
+
+       private void assertAttributeNode(String pathName,
+                       AttributesNode attributeNode, List<Attribute> nodeAttrs) {
+               if (attributeNode == null)
+                       assertTrue(nodeAttrs == null || nodeAttrs.isEmpty());
+               else {
+
+                       Map<String, Attribute> entryAttributes = new LinkedHashMap<String, Attribute>();
+                       attributeNode.getAttributes(pathName, false, entryAttributes);
+
+                       if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
+                               for (Attribute attribute : nodeAttrs) {
+                                       assertThat(entryAttributes.values(), hasItem(attribute));
+                               }
+                       } else {
+                               assertTrue(
+                                               "The entry "
+                                                               + pathName
+                                                               + " should not have any attributes. Instead, the following attributes are applied to this file "
+                                                               + entryAttributes.toString(),
+                                               entryAttributes.isEmpty());
+                       }
+               }
+       }
+
+       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() throws Exception {
+               TreeWalk newWalk = new TreeWalk(db);
+               newWalk.addTree(new DirCacheIterator(db.readDirCache()));
+               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/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
new file mode 100644 (file)
index 0000000..64b0535
--- /dev/null
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014, Obeo.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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 java.util.Arrays.asList;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.attributes.Attribute.State;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.junit.Test;
+
+/**
+ * Tests attributes node behavior on the local filesystem.
+ */
+public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase {
+
+       private static final FileMode D = FileMode.TREE;
+
+       private static final FileMode F = FileMode.REGULAR_FILE;
+
+       private static Attribute EOL_CRLF = new Attribute("eol", "crlf");
+
+       private static Attribute EOL_LF = new Attribute("eol", "lf");
+
+       private static Attribute DELTA_UNSET = new Attribute("delta", State.UNSET);
+
+       private static Attribute CUSTOM_VALUE = new Attribute("custom", "value");
+
+       private TreeWalk walk;
+
+       @Test
+       public void testRules() throws Exception {
+
+               File customAttributeFile = File.createTempFile("tmp_",
+                               "customAttributeFile", null);
+               customAttributeFile.deleteOnExit();
+
+               JGitTestUtil.write(customAttributeFile, "*.txt custom=value");
+               db.getConfig().setString("core", null, "attributesfile",
+                               customAttributeFile.getAbsolutePath());
+               writeAttributesFile(".git/info/attributes", "windows* eol=crlf");
+
+               writeAttributesFile(".gitattributes", "*.txt eol=lf");
+               writeTrashFile("windows.file", "");
+               writeTrashFile("windows.txt", "");
+               writeTrashFile("global.txt", "");
+               writeTrashFile("readme.txt", "");
+
+               writeAttributesFile("src/config/.gitattributes", "*.txt -delta");
+               writeTrashFile("src/config/readme.txt", "");
+               writeTrashFile("src/config/windows.file", "");
+               writeTrashFile("src/config/windows.txt", "");
+
+               walk = beginWalk();
+
+               assertIteration(F, ".gitattributes");
+               assertIteration(F, "global.txt", asList(EOL_LF), null,
+                               asList(CUSTOM_VALUE));
+               assertIteration(F, "readme.txt", asList(EOL_LF), null,
+                               asList(CUSTOM_VALUE));
+
+               assertIteration(D, "src");
+
+               assertIteration(D, "src/config");
+               assertIteration(F, "src/config/.gitattributes");
+               assertIteration(F, "src/config/readme.txt", asList(DELTA_UNSET), null,
+                               asList(CUSTOM_VALUE));
+               assertIteration(F, "src/config/windows.file", null, asList(EOL_CRLF),
+                               null);
+               assertIteration(F, "src/config/windows.txt", asList(DELTA_UNSET),
+                               asList(EOL_CRLF), asList(CUSTOM_VALUE));
+
+               assertIteration(F, "windows.file", null, asList(EOL_CRLF), null);
+               assertIteration(F, "windows.txt", asList(EOL_LF), asList(EOL_CRLF),
+                               asList(CUSTOM_VALUE));
+
+               endWalk();
+       }
+
+       /**
+        * Checks that if there is no .gitattributes file in the repository
+        * everything still work fine.
+        *
+        * @throws Exception
+        */
+       @Test
+       public void testNoAttributes() throws Exception {
+               writeTrashFile("l0.txt", "");
+               writeTrashFile("level1/l1.txt", "");
+               writeTrashFile("level1/level2/l2.txt", "");
+
+               walk = beginWalk();
+
+               assertIteration(F, "l0.txt");
+
+               assertIteration(D, "level1");
+               assertIteration(F, "level1/l1.txt");
+
+               assertIteration(D, "level1/level2");
+               assertIteration(F, "level1/level2/l2.txt");
+
+               endWalk();
+       }
+
+       /**
+        * Checks that empty .gitattribute files do not return incorrect value.
+        *
+        * @throws Exception
+        */
+       @Test
+       public void testEmptyGitAttributeFile() throws Exception {
+               writeAttributesFile(".git/info/attributes", "");
+               writeTrashFile("l0.txt", "");
+               writeAttributesFile(".gitattributes", "");
+               writeTrashFile("level1/l1.txt", "");
+               writeTrashFile("level1/level2/l2.txt", "");
+
+               walk = beginWalk();
+
+               assertIteration(F, ".gitattributes");
+               assertIteration(F, "l0.txt");
+
+               assertIteration(D, "level1");
+               assertIteration(F, "level1/l1.txt");
+
+               assertIteration(D, "level1/level2");
+               assertIteration(F, "level1/level2/l2.txt");
+
+               endWalk();
+       }
+
+       @Test
+       public void testNoMatchingAttributes() throws Exception {
+               writeAttributesFile(".git/info/attributes", "*.java delta");
+               writeAttributesFile(".gitattributes", "*.java -delta");
+               writeAttributesFile("levelA/.gitattributes", "*.java eol=lf");
+               writeAttributesFile("levelB/.gitattributes", "*.txt eol=lf");
+
+               writeTrashFile("levelA/lA.txt", "");
+
+               walk = beginWalk();
+
+               assertIteration(F, ".gitattributes");
+
+               assertIteration(D, "levelA");
+               assertIteration(F, "levelA/.gitattributes");
+               assertIteration(F, "levelA/lA.txt");
+
+               assertIteration(D, "levelB");
+               assertIteration(F, "levelB/.gitattributes");
+
+               endWalk();
+       }
+
+       private void assertIteration(FileMode type, String pathName)
+                       throws IOException {
+               assertIteration(type, pathName, Collections.<Attribute> emptyList(),
+                               Collections.<Attribute> emptyList(),
+                               Collections.<Attribute> emptyList());
+       }
+
+       private void assertIteration(FileMode type, String pathName,
+                       List<Attribute> nodeAttrs, List<Attribute> infoAttrs,
+                       List<Attribute> globalAttrs)
+                       throws IOException {
+               assertTrue("walk has entry", walk.next());
+               assertEquals(pathName, walk.getPathString());
+               assertEquals(type, walk.getFileMode(0));
+               WorkingTreeIterator itr = walk.getTree(0, WorkingTreeIterator.class);
+               assertNotNull("has tree", itr);
+
+               AttributesNode attributeNode = itr.getEntryAttributesNode();
+               assertAttributeNode(pathName, attributeNode, nodeAttrs);
+               AttributesNode infoAttributeNode = itr.getInfoAttributesNode();
+               assertAttributeNode(pathName, infoAttributeNode, infoAttrs);
+               AttributesNode globalAttributeNode = itr.getGlobalAttributesNode();
+               assertAttributeNode(pathName, globalAttributeNode, globalAttrs);
+               if (D.equals(type))
+                       walk.enterSubtree();
+
+       }
+
+       private void assertAttributeNode(String pathName,
+                       AttributesNode attributeNode, List<Attribute> nodeAttrs) {
+               if (attributeNode == null)
+                       assertTrue(nodeAttrs == null || nodeAttrs.isEmpty());
+               else {
+
+                       Map<String, Attribute> entryAttributes = new LinkedHashMap<String, Attribute>();
+                       attributeNode.getAttributes(pathName, false, entryAttributes);
+
+                       if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
+                               for (Attribute attribute : nodeAttrs) {
+                                       assertThat(entryAttributes.values(), hasItem(attribute));
+                               }
+                       } else {
+                               assertTrue(
+                                               "The entry "
+                                                               + pathName
+                                                               + " should not have any attributes. Instead, the following attributes are applied to this file "
+                                                               + entryAttributes.toString(),
+                                               entryAttributes.isEmpty());
+                       }
+               }
+       }
+
+       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() throws CorruptObjectException {
+               TreeWalk newWalk = new TreeWalk(db);
+               newWalk.addTree(new FileTreeIterator(db));
+               return newWalk;
+       }
+
+       private void endWalk() throws IOException {
+               assertFalse("Not all files tested", walk.next());
+       }
+}
index ae1136a361aafce71ba18d9ce5fbff719671e1b9..eea13ec7be9b036db1eb67bdfb17ee62e9098a6f 100644 (file)
@@ -16,10 +16,12 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
    org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.blame,
+   org.eclipse.jgit.submodule,
    org.eclipse.jgit.transport,
    org.eclipse.jgit.merge",
  org.eclipse.jgit.api.errors;version="3.7.0";
   uses:="org.eclipse.jgit.lib,org.eclipse.jgit.errors",
+ org.eclipse.jgit.attributes;version="3.7.0",
  org.eclipse.jgit.blame;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -36,7 +38,8 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util,
-   org.eclipse.jgit.events",
+   org.eclipse.jgit.events,
+   org.eclipse.jgit.attributes",
  org.eclipse.jgit.errors;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.internal.storage.pack,
@@ -47,7 +50,8 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
  org.eclipse.jgit.fnmatch;version="3.7.0",
  org.eclipse.jgit.gitrepo;version="3.7.0";
   uses:="org.eclipse.jgit.api,
-   org.eclipse.jgit.lib",
+   org.eclipse.jgit.lib,
+   org.eclipse.jgit.revwalk",
  org.eclipse.jgit.gitrepo.internal;version="3.7.0";x-internal:=true,
  org.eclipse.jgit.ignore;version="3.7.0",
  org.eclipse.jgit.ignore.internal;version="3.7.0";x-friends:="org.eclipse.jgit.test",
@@ -69,13 +73,15 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
    org.eclipse.jgit.dircache,
    org.eclipse.jgit.internal.storage.file,
    org.eclipse.jgit.treewalk,
-   org.eclipse.jgit.transport",
+   org.eclipse.jgit.transport,
+   org.eclipse.jgit.submodule",
  org.eclipse.jgit.merge;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.diff,
-   org.eclipse.jgit.dircache",
+   org.eclipse.jgit.dircache,
+   org.eclipse.jgit.api",
  org.eclipse.jgit.nls;version="3.7.0",
  org.eclipse.jgit.notes;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
@@ -85,7 +91,8 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
  org.eclipse.jgit.patch;version="3.7.0";
   uses:="org.eclipse.jgit.lib,org.eclipse.jgit.diff",
  org.eclipse.jgit.revplot;version="3.7.0";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.revwalk",
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.revwalk",
  org.eclipse.jgit.revwalk;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
@@ -99,7 +106,9 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
  org.eclipse.jgit.storage.pack;version="3.7.0";
   uses:="org.eclipse.jgit.lib",
  org.eclipse.jgit.submodule;version="3.7.0";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.treewalk,org.eclipse.jgit.treewalk.filter",
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.treewalk.filter,
+   org.eclipse.jgit.treewalk",
  org.eclipse.jgit.transport;version="3.7.0";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.revwalk,
@@ -115,17 +124,22 @@ Export-Package: org.eclipse.jgit.api;version="3.7.0";
  org.eclipse.jgit.transport.http;version="3.7.0";
   uses:="javax.net.ssl",
  org.eclipse.jgit.transport.resolver;version="3.7.0";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.transport",
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.transport",
  org.eclipse.jgit.treewalk;version="3.7.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
+   org.eclipse.jgit.attributes,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util,
    org.eclipse.jgit.dircache",
  org.eclipse.jgit.treewalk.filter;version="3.7.0";
   uses:="org.eclipse.jgit.treewalk",
  org.eclipse.jgit.util;version="3.7.0";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.transport.http,org.eclipse.jgit.storage.file",
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.transport.http,
+   org.eclipse.jgit.storage.file,
+   org.ietf.jgss",
  org.eclipse.jgit.util.io;version="3.7.0"
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
new file mode 100644 (file)
index 0000000..d3ce685
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2010, Marc Strapetz <marc.strapetz@syntevo.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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;
+
+/**
+ * Represents an attribute.
+ * <p>
+ * According to the man page, an attribute can have the following states:
+ * <ul>
+ * <li>Set - represented by {@link State#SET}</li>
+ * <li>Unset - represented by {@link State#UNSET}</li>
+ * <li>Set to a value - represented by {@link State#CUSTOM}</li>
+ * <li>Unspecified - <code>null</code> is used instead of an instance of this
+ * class</li>
+ * </ul>
+ * </p>
+ *
+ * @since 3.7
+ */
+public final class Attribute {
+
+       /**
+        * The attribute value state
+        */
+       public static enum State {
+               /** the attribute is set */
+               SET,
+
+               /** the attribute is unset */
+               UNSET,
+
+               /** the attribute is set to a custom value */
+               CUSTOM
+       }
+
+       private final String key;
+       private final State state;
+       private final String value;
+
+       /**
+        * Creates a new instance
+        *
+        * @param key
+        *            the attribute key. Should not be <code>null</code>.
+        * @param state
+        *            the attribute state. It should be either {@link State#SET} or
+        *            {@link State#UNSET}. In order to create a custom value
+        *            attribute prefer the use of {@link #Attribute(String, String)}
+        *            constructor.
+        */
+       public Attribute(String key, State state) {
+               this(key, state, null);
+       }
+
+       private Attribute(String key, State state, String value) {
+               if (key == null)
+                       throw new NullPointerException(
+                                       "The key of an attribute should not be null"); //$NON-NLS-1$
+               if (state == null)
+                       throw new NullPointerException(
+                                       "The state of an attribute should not be null"); //$NON-NLS-1$
+
+               this.key = key;
+               this.state = state;
+               this.value = value;
+       }
+
+       /**
+        * Creates a new instance.
+        *
+        * @param key
+        *            the attribute key. Should not be <code>null</code>.
+        * @param value
+        *            the custom attribute value
+        */
+       public Attribute(String key, String value) {
+               this(key, State.CUSTOM, value);
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (!(obj instanceof Attribute))
+                       return false;
+               Attribute other = (Attribute) obj;
+               if (!key.equals(other.key))
+                       return false;
+               if (state != other.state)
+                       return false;
+               if (value == null) {
+                       if (other.value != null)
+                               return false;
+               } else if (!value.equals(other.value))
+                       return false;
+               return true;
+       }
+
+       /**
+        * @return the attribute key (never returns <code>null</code>)
+        */
+       public String getKey() {
+               return key;
+       }
+
+       /**
+        * Returns the state.
+        *
+        * @return the state (never returns <code>null</code>)
+        */
+       public State getState() {
+               return state;
+       }
+
+       /**
+        * @return the attribute value (may be <code>null</code>)
+        */
+       public String getValue() {
+               return value;
+       }
+
+       @Override
+       public int hashCode() {
+               final int prime = 31;
+               int result = 1;
+               result = prime * result + key.hashCode();
+               result = prime * result + state.hashCode();
+               result = prime * result + ((value == null) ? 0 : value.hashCode());
+               return result;
+       }
+
+       @Override
+       public String toString() {
+               switch (state) {
+               case SET:
+                       return key;
+               case UNSET:
+                       return "-" + key; //$NON-NLS-1$
+               case CUSTOM:
+               default:
+                       return key + "=" + value; //$NON-NLS-1$
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNode.java
new file mode 100644 (file)
index 0000000..70f56ff
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.eclipse.jgit.lib.Constants;
+
+/**
+ * Represents a bundle of attributes inherited from a base directory.
+ *
+ * This class is not thread safe, it maintains state about the last match.
+ *
+ * @since 3.7
+ */
+public class AttributesNode {
+       /** The rules that have been parsed into this node. */
+       private final List<AttributesRule> rules;
+
+       /** Create an empty ignore node with no rules. */
+       public AttributesNode() {
+               rules = new ArrayList<AttributesRule>();
+       }
+
+       /**
+        * Create an ignore node with given rules.
+        *
+        * @param rules
+        *            list of rules.
+        **/
+       public AttributesNode(List<AttributesRule> rules) {
+               this.rules = rules;
+       }
+
+       /**
+        * Parse files according to gitattribute standards.
+        *
+        * @param in
+        *            input stream holding the standard ignore format. The caller is
+        *            responsible for closing the stream.
+        * @throws IOException
+        *             Error thrown when reading an ignore file.
+        */
+       public void parse(InputStream in) throws IOException {
+               BufferedReader br = asReader(in);
+               String txt;
+               while ((txt = br.readLine()) != null) {
+                       txt = txt.trim();
+                       if (txt.length() > 0 && !txt.startsWith("#") /* Comments *///$NON-NLS-1$
+                                       && !txt.startsWith("!") /* Negative pattern forbidden for attributes */) { //$NON-NLS-1$
+                               int patternEndSpace = txt.indexOf(' ');
+                               int patternEndTab = txt.indexOf('\t');
+
+                               final int patternEnd;
+                               if (patternEndSpace == -1)
+                                       patternEnd = patternEndTab;
+                               else if (patternEndTab == -1)
+                                       patternEnd = patternEndSpace;
+                               else
+                                       patternEnd = Math.min(patternEndSpace, patternEndTab);
+
+                               if (patternEnd > -1)
+                                       rules.add(new AttributesRule(txt.substring(0, patternEnd),
+                                                       txt.substring(patternEnd + 1).trim()));
+                       }
+               }
+       }
+
+       private static BufferedReader asReader(InputStream in) {
+               return new BufferedReader(new InputStreamReader(in, Constants.CHARSET));
+       }
+
+       /** @return list of all ignore rules held by this node. */
+       public List<AttributesRule> getRules() {
+               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.
+        */
+       public void getAttributes(String entryPath, boolean isDirectory,
+                       Map<String, Attribute> 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.getKey(), 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
new file mode 100644 (file)
index 0000000..bcac14b
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * 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.eclipse.jgit.ignore.internal.IMatcher.NO_MATCH;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.attributes.Attribute.State;
+import org.eclipse.jgit.errors.InvalidPatternException;
+import org.eclipse.jgit.ignore.FastIgnoreRule;
+import org.eclipse.jgit.ignore.internal.IMatcher;
+import org.eclipse.jgit.ignore.internal.PathMatcher;
+
+/**
+ * A single attributes rule corresponding to one line in a .gitattributes file.
+ *
+ * Inspiration from: {@link FastIgnoreRule}
+ *
+ * @since 3.7
+ */
+public class AttributesRule {
+
+       /**
+        * regular expression for splitting attributes - space, tab and \r (the C
+        * implementation oddly enough allows \r between attributes)
+        * */
+       private static final String ATTRIBUTES_SPLIT_REGEX = "[ \t\r]"; //$NON-NLS-1$
+
+       private static List<Attribute> parseAttributes(String attributesLine) {
+               // the C implementation oddly enough allows \r between attributes too.
+               ArrayList<Attribute> result = new ArrayList<Attribute>();
+               for (String attribute : attributesLine.split(ATTRIBUTES_SPLIT_REGEX)) {
+                       attribute = attribute.trim();
+                       if (attribute.length() == 0)
+                               continue;
+
+                       if (attribute.startsWith("-")) {//$NON-NLS-1$
+                               if (attribute.length() > 1)
+                                       result.add(new Attribute(attribute.substring(1),
+                                                       State.UNSET));
+                               continue;
+                       }
+
+                       final int equalsIndex = attribute.indexOf("="); //$NON-NLS-1$
+                       if (equalsIndex == -1)
+                               result.add(new Attribute(attribute, State.SET));
+                       else {
+                               String attributeKey = attribute.substring(0, equalsIndex);
+                               if (attributeKey.length() > 0) {
+                                       String attributeValue = attribute
+                                                       .substring(equalsIndex + 1);
+                                       result.add(new Attribute(attributeKey, attributeValue));
+                               }
+                       }
+               }
+               return result;
+       }
+
+       private final String pattern;
+       private final List<Attribute> attributes;
+
+       private boolean nameOnly;
+       private boolean dirOnly;
+
+       private IMatcher matcher;
+
+       /**
+        * Create a new attribute rule with the given pattern. Assumes that the
+        * pattern is already trimmed.
+        *
+        * @param pattern
+        *            Base pattern for the attributes rule. This pattern will be
+        *            parsed to generate rule parameters. It can not be
+        *            <code>null</code>.
+        * @param attributes
+        *            the rule attributes. This string will be parsed to read the
+        *            attributes.
+        */
+       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;
+               }
+
+               boolean hasSlash = pattern.contains("/"); //$NON-NLS-1$
+
+               if (!hasSlash)
+                       nameOnly = true;
+               else if (!pattern.startsWith("/")) { //$NON-NLS-1$
+                       // Contains "/" but does not start with one
+                       // Adding / to the start should not interfere with matching
+                       pattern = "/" + pattern; //$NON-NLS-1$
+               }
+
+               try {
+                       matcher = PathMatcher.createPathMatcher(pattern,
+                                       Character.valueOf(FastIgnoreRule.PATH_SEPARATOR), dirOnly);
+               } catch (InvalidPatternException e) {
+                       matcher = NO_MATCH;
+               }
+
+               this.pattern = pattern;
+       }
+
+       /**
+        * @return True if the pattern should match directories only
+        */
+       public boolean dirOnly() {
+               return dirOnly;
+       }
+
+       /**
+        * Returns the attributes.
+        *
+        * @return an unmodifiable list of attributes (never returns
+        *         <code>null</code>)
+        */
+       public List<Attribute> getAttributes() {
+               return Collections.unmodifiableList(attributes);
+       }
+
+       /**
+        * @return <code>true</code> if the pattern is just a file name and not a
+        *         path
+        */
+       public boolean isNameOnly() {
+               return nameOnly;
+       }
+
+       /**
+        * @return The blob pattern to be used as a matcher (never returns
+        *         <code>null</code>)
+        */
+       public String getPattern() {
+               return pattern;
+       }
+
+       /**
+        * Returns <code>true</code> if a match was made.
+        *
+        * @param relativeTarget
+        *            Name pattern of the file, relative to the base directory of
+        *            this rule
+        * @param isDirectory
+        *            Whether the target file is a directory or not
+        * @return True if a match was made.
+        */
+       public boolean isMatch(String relativeTarget, boolean isDirectory) {
+               if (relativeTarget == null)
+                       return false;
+               if (relativeTarget.length() == 0)
+                       return false;
+               boolean match = matcher.matches(relativeTarget, isDirectory);
+               return match;
+       }
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/package-info.java
new file mode 100644 (file)
index 0000000..5d133d8
--- /dev/null
@@ -0,0 +1,4 @@
+/**
+ * Support for reading .gitattributes.
+ */
+package org.eclipse.jgit.attributes;
index 706e0574801313bb509a73caae3f4157d0fc43b2..354a07439af8afb5a84509c91381c145547fce8e 100644 (file)
 package org.eclipse.jgit.dircache;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
 
+import org.eclipse.jgit.attributes.AttributesNode;
+import org.eclipse.jgit.attributes.AttributesRule;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+import org.eclipse.jgit.util.RawParseUtils;
 
 /**
  * Iterate a {@link DirCache} as part of a <code>TreeWalk</code>.
@@ -65,6 +72,10 @@ import org.eclipse.jgit.treewalk.EmptyTreeIterator;
  * @see org.eclipse.jgit.treewalk.TreeWalk
  */
 public class DirCacheIterator extends AbstractTreeIterator {
+       /** Byte array holding ".gitattributes" string */
+       private static final byte[] DOT_GIT_ATTRIBUTES_BYTES = Constants.DOT_GIT_ATTRIBUTES
+                       .getBytes();
+
        /** The cache this iterator was created to walk. */
        protected final DirCache cache;
 
@@ -92,6 +103,9 @@ public class DirCacheIterator extends AbstractTreeIterator {
        /** The subtree containing {@link #currentEntry} if this is first entry. */
        protected DirCacheTree currentSubtree;
 
+       /** Holds an {@link AttributesNode} for the current entry */
+       private AttributesNode attributesNode;
+
        /**
         * Create a new iterator for an already loaded DirCache instance.
         * <p>
@@ -254,6 +268,10 @@ public class DirCacheIterator extends AbstractTreeIterator {
                path = cep;
                pathLen = cep.length;
                currentSubtree = null;
+               // Checks if this entry is a .gitattributes file
+               if (RawParseUtils.match(path, pathOffset, DOT_GIT_ATTRIBUTES_BYTES) == path.length)
+                       attributesNode = new LazyLoadingAttributesNode(
+                                       currentEntry.getObjectId());
        }
 
        /**
@@ -265,4 +283,50 @@ public class DirCacheIterator extends AbstractTreeIterator {
        public DirCacheEntry getDirCacheEntry() {
                return currentSubtree == null ? currentEntry : null;
        }
+
+       /**
+        * Retrieves the {@link AttributesNode} for the current entry.
+        *
+        * @param reader
+        *            {@link ObjectReader} used to parse the .gitattributes entry.
+        * @return {@link AttributesNode} for the current entry.
+        * @throws IOException
+        * @since 3.7
+        */
+       public AttributesNode getEntryAttributesNode(ObjectReader reader)
+                       throws IOException {
+               if (attributesNode instanceof LazyLoadingAttributesNode)
+                       attributesNode = ((LazyLoadingAttributesNode) attributesNode)
+                                       .load(reader);
+               return attributesNode;
+       }
+
+       /**
+        * {@link AttributesNode} implementation that provides lazy loading
+        * facilities.
+        */
+       private static class LazyLoadingAttributesNode extends AttributesNode {
+               final ObjectId objectId;
+
+               LazyLoadingAttributesNode(ObjectId objectId) {
+                       super(Collections.<AttributesRule> emptyList());
+                       this.objectId = objectId;
+
+               }
+
+               AttributesNode load(ObjectReader reader) throws IOException {
+                       AttributesNode r = new AttributesNode();
+                       ObjectLoader loader = reader.open(objectId);
+                       if (loader != null) {
+                               InputStream in = loader.openStream();
+                               try {
+                                       r.parse(in);
+                               } finally {
+                                       in.close();
+                               }
+                       }
+                       return r.getRules().isEmpty() ? null : r;
+               }
+       }
+
 }
index 02863bd16a346ca388f35084a0f69b5d77d1ad74..2303ffd6d6f41884bb6598f93f90d1f93bef243b 100644 (file)
@@ -43,7 +43,7 @@
 package org.eclipse.jgit.ignore;
 
 import static org.eclipse.jgit.ignore.internal.Strings.stripTrailing;
-
+import static org.eclipse.jgit.ignore.internal.IMatcher.NO_MATCH;
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.ignore.internal.IMatcher;
 import org.eclipse.jgit.ignore.internal.PathMatcher;
@@ -63,8 +63,6 @@ public class FastIgnoreRule {
         */
        public static final char PATH_SEPARATOR = '/';
 
-       private static final NoResultMatcher NO_MATCH = new NoResultMatcher();
-
        private final IMatcher matcher;
 
        private final boolean inverse;
@@ -214,16 +212,4 @@ public class FastIgnoreRule {
                        return false;
                return matcher.equals(other.matcher);
        }
-
-       static final class NoResultMatcher implements IMatcher {
-
-               public boolean matches(String path, boolean assumeDirectory) {
-                       return false;
-               }
-
-               public boolean matches(String segment, int startIncl, int endExcl,
-                               boolean assumeDirectory) {
-                       return false;
-               }
-       }
 }
index 10b5e49e1fcc621c76215818b491be212a4eecdb..8bb4dfb564ba99ae52716cb574260cfc12e1a82c 100644 (file)
@@ -49,6 +49,20 @@ package org.eclipse.jgit.ignore.internal;
  */
 public interface IMatcher {
 
+       /**
+        * Matcher that does not match any pattern.
+        */
+       public static final IMatcher NO_MATCH = new IMatcher() {
+               public boolean matches(String path, boolean assumeDirectory) {
+                       return false;
+               }
+
+               public boolean matches(String segment, int startIncl, int endExcl,
+                               boolean assumeDirectory) {
+                       return false;
+               }
+       };
+
        /**
         * Matches entire given string
         *
index ccbfed720ad85ce0699ccd2cc1ff1808a7b2689e..8a2080bac866bfe46257cff4bb4470153ca709a9 100644 (file)
@@ -113,6 +113,13 @@ public class ConfigConstants {
        /** The "excludesfile" key */
        public static final String CONFIG_KEY_EXCLUDESFILE = "excludesfile";
 
+       /**
+        * The "attributesfile" key
+        *
+        * @since 3.7
+        */
+       public static final String CONFIG_KEY_ATTRIBUTESFILE = "attributesfile";
+
        /** The "filemode" key */
        public static final String CONFIG_KEY_FILEMODE = "filemode";
 
index f149749843c7d8bfab9251ade87db81052961c4d..705d54cfa390630b490f4727a07dd0a795783613 100644 (file)
@@ -356,6 +356,13 @@ public final class Constants {
        /** A bare repository typically ends with this string */
        public static final String DOT_GIT_EXT = ".git";
 
+       /**
+        * Name of the attributes file
+        *
+        * @since 3.7
+        */
+       public static final String DOT_GIT_ATTRIBUTES = ".gitattributes";
+
        /** Name of the ignore file */
        public static final String DOT_GIT_IGNORE = ".gitignore";
 
index 8f31d96de60d3e08d671a7918470c95d8b38c7fd..5a7634a6f17e01e4ba974d28efcb92d9e98096ba 100644 (file)
@@ -1,4 +1,5 @@
 /*
+ * Copyright (C) 2013, Gunnar Wagenknecht
  * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
  * Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2009, Google Inc.
@@ -101,6 +102,8 @@ public class CoreConfig {
 
        private final String excludesfile;
 
+       private final String attributesfile;
+
        /**
         * Options for symlink handling
         *
@@ -136,6 +139,8 @@ public class CoreConfig {
                                ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
                excludesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null,
                                ConfigConstants.CONFIG_KEY_EXCLUDESFILE);
+               attributesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION,
+                               null, ConfigConstants.CONFIG_KEY_ATTRIBUTESFILE);
        }
 
        /**
@@ -165,4 +170,12 @@ public class CoreConfig {
        public String getExcludesFile() {
                return excludesfile;
        }
+
+       /**
+        * @return path of attributesfile
+        * @since 3.7
+        */
+       public String getAttributesFile() {
+               return attributesfile;
+       }
 }
index 6311da6b68c234ae281d03a560234758f2c62502..3838149a4fb6152b8aaf56ed4d1cb516c3d94046 100644 (file)
@@ -63,6 +63,8 @@ import java.util.Collections;
 import java.util.Comparator;
 
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.attributes.AttributesNode;
+import org.eclipse.jgit.attributes.AttributesRule;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -133,6 +135,9 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
        /** If there is a .gitignore file present, the parsed rules from it. */
        private IgnoreNode ignoreNode;
 
+       /** If there is a .gitattributes file present, the parsed rules from it. */
+       private AttributesNode attributesNode;
+
        /** Repository that is the root level being iterated over */
        protected Repository repository;
 
@@ -142,6 +147,19 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
        /** The offset of the content id in {@link #idBuffer()} */
        private int contentIdOffset;
 
+       /**
+        * Holds the {@link AttributesNode} that is stored in
+        * $GIT_DIR/info/attributes file.
+        */
+       private AttributesNode infoAttributeNode;
+
+       /**
+        * Holds the {@link AttributesNode} that is stored in global attribute file.
+        *
+        * @see CoreConfig#getAttributesFile()
+        */
+       private AttributesNode globalAttributeNode;
+
        /**
         * Create a new iterator with no parent.
         *
@@ -185,6 +203,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
        protected WorkingTreeIterator(final WorkingTreeIterator p) {
                super(p);
                state = p.state;
+               infoAttributeNode = p.infoAttributeNode;
+               globalAttributeNode = p.globalAttributeNode;
        }
 
        /**
@@ -204,6 +224,10 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                else
                        entry = null;
                ignoreNode = new RootIgnoreNode(entry, repo);
+
+               infoAttributeNode = new InfoAttributesNode(repo);
+
+               globalAttributeNode = new GlobalAttributesNode(repo);
        }
 
        /**
@@ -626,6 +650,56 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                return ignoreNode;
        }
 
+       /**
+        * Retrieves the {@link AttributesNode} for the current entry.
+        *
+        * @return {@link AttributesNode} for the current entry.
+        * @throws IOException
+        *             if an error is raised while parsing the .gitattributes file
+        * @since 3.7
+        */
+       public AttributesNode getEntryAttributesNode() throws IOException {
+               if (attributesNode instanceof PerDirectoryAttributesNode)
+                       attributesNode = ((PerDirectoryAttributesNode) attributesNode)
+                                       .load();
+               return attributesNode;
+       }
+
+       /**
+        * Retrieves the {@link AttributesNode} that holds the information located
+        * in $GIT_DIR/info/attributes file.
+        *
+        * @return the {@link AttributesNode} that holds the information located in
+        *         $GIT_DIR/info/attributes file.
+        * @throws IOException
+        *             if an error is raised while parsing the attributes file
+        * @since 3.7
+        */
+       public AttributesNode getInfoAttributesNode() throws IOException {
+               if (infoAttributeNode instanceof InfoAttributesNode)
+                       infoAttributeNode = ((InfoAttributesNode) infoAttributeNode).load();
+               return infoAttributeNode;
+       }
+
+       /**
+        * Retrieves the {@link AttributesNode} that holds the information located
+        * in system-wide file.
+        *
+        * @return the {@link AttributesNode} that holds the information located in
+        *         system-wide file.
+        * @throws IOException
+        *             IOException if an error is raised while parsing the
+        *             attributes file
+        * @see CoreConfig#getAttributesFile()
+        * @since 3.7
+        */
+       public AttributesNode getGlobalAttributesNode() throws IOException {
+               if (globalAttributeNode instanceof GlobalAttributesNode)
+                       globalAttributeNode = ((GlobalAttributesNode) globalAttributeNode)
+                                       .load();
+               return globalAttributeNode;
+       }
+
        private static final Comparator<Entry> ENTRY_CMP = new Comparator<Entry>() {
                public int compare(final Entry o1, final Entry o2) {
                        final byte[] a = o1.encodedName;
@@ -679,6 +753,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                                continue;
                        if (Constants.DOT_GIT_IGNORE.equals(name))
                                ignoreNode = new PerDirectoryIgnoreNode(e);
+                       if (Constants.DOT_GIT_ATTRIBUTES.equals(name))
+                               attributesNode = new PerDirectoryAttributesNode(e);
                        if (i != o)
                                entries[o] = e;
                        e.encodeName(nameEncoder);
@@ -1223,6 +1299,90 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                }
        }
 
+       /** Magic type indicating we know rules exist, but they aren't loaded. */
+       private static class PerDirectoryAttributesNode extends AttributesNode {
+               final Entry entry;
+
+               PerDirectoryAttributesNode(Entry entry) {
+                       super(Collections.<AttributesRule> emptyList());
+                       this.entry = entry;
+               }
+
+               AttributesNode load() throws IOException {
+                       AttributesNode r = new AttributesNode();
+                       InputStream in = entry.openInputStream();
+                       try {
+                               r.parse(in);
+                       } finally {
+                               in.close();
+                       }
+                       return r.getRules().isEmpty() ? null : r;
+               }
+       }
+
+       /**
+        * Attributes node loaded from global system-wide file.
+        */
+       private static class GlobalAttributesNode extends AttributesNode {
+               final Repository repository;
+
+               GlobalAttributesNode(Repository repository) {
+                       this.repository = repository;
+               }
+
+               AttributesNode load() throws IOException {
+                       AttributesNode r = new AttributesNode();
+
+                       FS fs = repository.getFS();
+                       String path = repository.getConfig().get(CoreConfig.KEY)
+                                       .getAttributesFile();
+                       if (path != null) {
+                               File attributesFile;
+                               if (path.startsWith("~/")) //$NON-NLS-1$
+                                       attributesFile = fs.resolve(fs.userHome(),
+                                                       path.substring(2));
+                               else
+                                       attributesFile = fs.resolve(null, path);
+                               loadRulesFromFile(r, attributesFile);
+                       }
+                       return r.getRules().isEmpty() ? null : r;
+               }
+       }
+
+       /** Magic type indicating there may be rules for the top level. */
+       private static class InfoAttributesNode extends AttributesNode {
+               final Repository repository;
+
+               InfoAttributesNode(Repository repository) {
+                       this.repository = repository;
+               }
+
+               AttributesNode load() throws IOException {
+                       AttributesNode r = new AttributesNode();
+
+                       FS fs = repository.getFS();
+
+                       File attributes = fs.resolve(repository.getDirectory(),
+                                       "info/attributes"); //$NON-NLS-1$
+                       loadRulesFromFile(r, attributes);
+
+                       return r.getRules().isEmpty() ? null : r;
+               }
+
+       }
+
+       private static void loadRulesFromFile(AttributesNode r, File attrs)
+                       throws FileNotFoundException, IOException {
+               if (attrs.exists()) {
+                       FileInputStream in = new FileInputStream(attrs);
+                       try {
+                               r.parse(in);
+                       } finally {
+                               in.close();
+                       }
+               }
+       }
+
        private static final class IteratorState {
                /** Options used to process the working tree. */
                final WorkingTreeOptions options;