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