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
/* | |||||
* 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()); | |||||
} | |||||
} |
public void testGetters() { | public void testGetters() { | ||||
AttributesRule r = new AttributesRule("/pattern/", ""); | AttributesRule r = new AttributesRule("/pattern/", ""); | ||||
assertFalse(r.isNameOnly()); | assertFalse(r.isNameOnly()); | ||||
assertTrue(r.dirOnly()); | |||||
assertTrue(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertTrue(r.getAttributes().isEmpty()); | assertTrue(r.getAttributes().isEmpty()); | ||||
assertEquals(r.getPattern(), "/pattern"); | assertEquals(r.getPattern(), "/pattern"); | ||||
r = new AttributesRule("/patter?/", ""); | r = new AttributesRule("/patter?/", ""); | ||||
assertFalse(r.isNameOnly()); | assertFalse(r.isNameOnly()); | ||||
assertTrue(r.dirOnly()); | |||||
assertTrue(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertTrue(r.getAttributes().isEmpty()); | assertTrue(r.getAttributes().isEmpty()); | ||||
assertEquals(r.getPattern(), "/patter?"); | assertEquals(r.getPattern(), "/patter?"); | ||||
r = new AttributesRule("patt*", ""); | r = new AttributesRule("patt*", ""); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertTrue(r.getAttributes().isEmpty()); | assertTrue(r.getAttributes().isEmpty()); | ||||
assertEquals(r.getPattern(), "patt*"); | assertEquals(r.getPattern(), "patt*"); | ||||
r = new AttributesRule("pattern", "attribute1"); | r = new AttributesRule("pattern", "attribute1"); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertFalse(r.getAttributes().isEmpty()); | assertFalse(r.getAttributes().isEmpty()); | ||||
assertEquals(r.getAttributes().size(), 1); | assertEquals(r.getAttributes().size(), 1); | ||||
r = new AttributesRule("pattern", "attribute1 -attribute2"); | r = new AttributesRule("pattern", "attribute1 -attribute2"); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertEquals(r.getAttributes().size(), 2); | assertEquals(r.getAttributes().size(), 2); | ||||
assertEquals(r.getPattern(), "pattern"); | assertEquals(r.getPattern(), "pattern"); | ||||
r = new AttributesRule("pattern", "attribute1 \t-attribute2 \t"); | r = new AttributesRule("pattern", "attribute1 \t-attribute2 \t"); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertEquals(r.getAttributes().size(), 2); | assertEquals(r.getAttributes().size(), 2); | ||||
assertEquals(r.getPattern(), "pattern"); | assertEquals(r.getPattern(), "pattern"); | ||||
r = new AttributesRule("pattern", "attribute1\t-attribute2\t"); | r = new AttributesRule("pattern", "attribute1\t-attribute2\t"); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertEquals(r.getAttributes().size(), 2); | assertEquals(r.getAttributes().size(), 2); | ||||
assertEquals(r.getPattern(), "pattern"); | assertEquals(r.getPattern(), "pattern"); | ||||
r = new AttributesRule("pattern", "attribute1\t -attribute2\t "); | r = new AttributesRule("pattern", "attribute1\t -attribute2\t "); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertEquals(r.getAttributes().size(), 2); | assertEquals(r.getAttributes().size(), 2); | ||||
assertEquals(r.getPattern(), "pattern"); | assertEquals(r.getPattern(), "pattern"); | ||||
r = new AttributesRule("pattern", | r = new AttributesRule("pattern", | ||||
"attribute1 -attribute2 attribute3=value "); | "attribute1 -attribute2 attribute3=value "); | ||||
assertTrue(r.isNameOnly()); | assertTrue(r.isNameOnly()); | ||||
assertFalse(r.dirOnly()); | |||||
assertFalse(r.isDirOnly()); | |||||
assertNotNull(r.getAttributes()); | assertNotNull(r.getAttributes()); | ||||
assertEquals(r.getAttributes().size(), 3); | assertEquals(r.getAttributes().size(), 3); | ||||
assertEquals(r.getPattern(), "pattern"); | assertEquals(r.getPattern(), "pattern"); |
} | } | ||||
private void assertAttributesNode(String pathName, | private void assertAttributesNode(String pathName, | ||||
AttributesNode attributesNode, List<Attribute> nodeAttrs) { | |||||
AttributesNode attributesNode, List<Attribute> nodeAttrs) | |||||
throws IOException { | |||||
if (attributesNode == null) | if (attributesNode == null) | ||||
assertTrue(nodeAttrs == null || nodeAttrs.isEmpty()); | assertTrue(nodeAttrs == null || nodeAttrs.isEmpty()); | ||||
else { | else { | ||||
Attributes entryAttributes = new Attributes(); | Attributes entryAttributes = new Attributes(); | ||||
attributesNode.getAttributes(pathName, | |||||
false, entryAttributes); | |||||
new AttributesHandler(walk).mergeAttributes(attributesNode, | |||||
pathName, | |||||
false, | |||||
entryAttributes); | |||||
if (nodeAttrs != null && !nodeAttrs.isEmpty()) { | if (nodeAttrs != null && !nodeAttrs.isEmpty()) { | ||||
for (Attribute attribute : nodeAttrs) { | for (Attribute attribute : nodeAttrs) { |
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | 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.After; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
* Test {@link AttributesNode} | * Test {@link AttributesNode} | ||||
*/ | */ | ||||
public class AttributesNodeTest { | 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); | private static final Attribute A_SET_ATTR = new Attribute("A", SET); | ||||
} | } | ||||
private void assertAttribute(String path, AttributesNode node, | private void assertAttribute(String path, AttributesNode node, | ||||
Attributes attrs) { | |||||
Attributes attrs) throws IOException { | |||||
Attributes attributes = new Attributes(); | Attributes attributes = new Attributes(); | ||||
node.getAttributes(path, false, attributes); | |||||
new AttributesHandler(DUMMY_WALK).mergeAttributes(node, path, false, | |||||
attributes); | |||||
assertEquals(attrs, attributes); | assertEquals(attrs, attributes); | ||||
} | } | ||||
} | } | ||||
private void assertAttributesNode(String pathName, | private void assertAttributesNode(String pathName, | ||||
AttributesNode attributesNode, List<Attribute> nodeAttrs) { | |||||
AttributesNode attributesNode, List<Attribute> nodeAttrs) | |||||
throws IOException { | |||||
if (attributesNode == null) | if (attributesNode == null) | ||||
assertTrue(nodeAttrs == null || nodeAttrs.isEmpty()); | assertTrue(nodeAttrs == null || nodeAttrs.isEmpty()); | ||||
else { | else { | ||||
Attributes entryAttributes = new Attributes(); | Attributes entryAttributes = new Attributes(); | ||||
attributesNode.getAttributes(pathName, | |||||
false, entryAttributes); | |||||
new AttributesHandler(walk).mergeAttributes(attributesNode, | |||||
pathName, false, | |||||
entryAttributes); | |||||
if (nodeAttrs != null && !nodeAttrs.isEmpty()) { | if (nodeAttrs != null && !nodeAttrs.isEmpty()) { | ||||
for (Attribute attribute : nodeAttrs) { | for (Attribute attribute : nodeAttrs) { |
<?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> |
/* | |||||
* 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; | |||||
} | |||||
} |
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.ListIterator; | |||||
import org.eclipse.jgit.lib.Constants; | import org.eclipse.jgit.lib.Constants; | ||||
return Collections.unmodifiableList(rules); | 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); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | } |
private final String pattern; | private final String pattern; | ||||
private final List<Attribute> attributes; | 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 | * Create a new attribute rule with the given pattern. Assumes that the | ||||
*/ | */ | ||||
public AttributesRule(String pattern, String attributes) { | public AttributesRule(String pattern, String attributes) { | ||||
this.attributes = parseAttributes(attributes); | this.attributes = parseAttributes(attributes); | ||||
nameOnly = false; | |||||
dirOnly = false; | |||||
if (pattern.endsWith("/")) { //$NON-NLS-1$ | if (pattern.endsWith("/")) { //$NON-NLS-1$ | ||||
pattern = pattern.substring(0, pattern.length() - 1); | pattern = pattern.substring(0, pattern.length() - 1); | ||||
dirOnly = true; | dirOnly = true; | ||||
} else { | |||||
dirOnly = false; | |||||
} | } | ||||
boolean hasSlash = pattern.contains("/"); //$NON-NLS-1$ | |||||
int slashIndex = pattern.indexOf('/'); | |||||
if (!hasSlash) | |||||
if (slashIndex < 0) { | |||||
nameOnly = true; | 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 | // Contains "/" but does not start with one | ||||
// Adding / to the start should not interfere with matching | // Adding / to the start should not interfere with matching | ||||
pattern = "/" + pattern; //$NON-NLS-1$ | pattern = "/" + pattern; //$NON-NLS-1$ | ||||
} | } | ||||
IMatcher candidateMatcher = NO_MATCH; | |||||
try { | try { | ||||
matcher = PathMatcher.createPathMatcher(pattern, | |||||
candidateMatcher = PathMatcher.createPathMatcher(pattern, | |||||
Character.valueOf(FastIgnoreRule.PATH_SEPARATOR), dirOnly); | Character.valueOf(FastIgnoreRule.PATH_SEPARATOR), dirOnly); | ||||
} catch (InvalidPatternException e) { | } catch (InvalidPatternException e) { | ||||
matcher = NO_MATCH; | |||||
// ignore: invalid patterns are silently ignored | |||||
} | } | ||||
this.matcher = candidateMatcher; | |||||
this.pattern = pattern; | this.pattern = pattern; | ||||
} | } | ||||
/** | /** | ||||
* @return True if the pattern should match directories only | * @return True if the pattern should match directories only | ||||
* @since 4.3 | |||||
*/ | */ | ||||
public boolean dirOnly() { | |||||
public boolean isDirOnly() { | |||||
return dirOnly; | return dirOnly; | ||||
} | } | ||||
import java.nio.CharBuffer; | import java.nio.CharBuffer; | ||||
import org.eclipse.jgit.attributes.AttributesNode; | import org.eclipse.jgit.attributes.AttributesNode; | ||||
import org.eclipse.jgit.attributes.AttributesHandler; | |||||
import org.eclipse.jgit.dircache.DirCacheCheckout; | import org.eclipse.jgit.dircache.DirCacheCheckout; | ||||
import org.eclipse.jgit.errors.CorruptObjectException; | import org.eclipse.jgit.errors.CorruptObjectException; | ||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException; | import org.eclipse.jgit.errors.IncorrectObjectTypeException; | ||||
/** A dummy object id buffer that matches the zero ObjectId. */ | /** A dummy object id buffer that matches the zero ObjectId. */ | ||||
protected static final byte[] zeroid = new byte[Constants.OBJECT_ID_LENGTH]; | 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. */ | /** The iterator this current entry is path equal to. */ | ||||
AbstractTreeIterator matches; | AbstractTreeIterator matches; |
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | import java.util.Set; | ||||
import org.eclipse.jgit.annotations.Nullable; | |||||
import org.eclipse.jgit.api.errors.JGitInternalException; | import org.eclipse.jgit.api.errors.JGitInternalException; | ||||
import org.eclipse.jgit.attributes.Attribute; | import org.eclipse.jgit.attributes.Attribute; | ||||
import org.eclipse.jgit.attributes.Attribute.State; | |||||
import org.eclipse.jgit.attributes.Attributes; | import org.eclipse.jgit.attributes.Attributes; | ||||
import org.eclipse.jgit.attributes.AttributesNode; | |||||
import org.eclipse.jgit.attributes.AttributesNodeProvider; | import org.eclipse.jgit.attributes.AttributesNodeProvider; | ||||
import org.eclipse.jgit.attributes.AttributesProvider; | import org.eclipse.jgit.attributes.AttributesProvider; | ||||
import org.eclipse.jgit.dircache.DirCacheBuildIterator; | import org.eclipse.jgit.dircache.DirCacheBuildIterator; | ||||
import org.eclipse.jgit.attributes.AttributesHandler; | |||||
import org.eclipse.jgit.dircache.DirCacheIterator; | import org.eclipse.jgit.dircache.DirCacheIterator; | ||||
import org.eclipse.jgit.errors.CorruptObjectException; | import org.eclipse.jgit.errors.CorruptObjectException; | ||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException; | import org.eclipse.jgit.errors.IncorrectObjectTypeException; | ||||
/** Cached attribute for the current entry */ | /** Cached attribute for the current entry */ | ||||
private Attributes attrs = null; | private Attributes attrs = null; | ||||
/** Cached attributes handler */ | |||||
private AttributesHandler attributesHandler; | |||||
private Config config; | private Config config; | ||||
/** | /** | ||||
return reader; | return reader; | ||||
} | } | ||||
/** | |||||
* @return the {@link OperationType} | |||||
* @since 4.3 | |||||
*/ | |||||
public OperationType getOperationType() { | |||||
return operationType; | |||||
} | |||||
/** | /** | ||||
* Release any resources used by this walker's reader. | * Release any resources used by this walker's reader. | ||||
* <p> | * <p> | ||||
attributesNodeProvider = provider; | 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. */ | /** Reset this walker so new tree iterators can be added to it. */ | ||||
public void reset() { | public void reset() { | ||||
attrs = null; | attrs = null; | ||||
attributesHandler = null; | |||||
trees = NO_TREES; | trees = NO_TREES; | ||||
advance = false; | advance = false; | ||||
depth = 0; | depth = 0; | ||||
return FileMode.fromBits(getRawMode(nth)); | 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. | * Obtain the ObjectId for the current entry. | ||||
* <p> | * <p> | ||||
} | } | ||||
/** | /** | ||||
* 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) { | 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++) { | for (int i = 0; i < trees.length; i++) { | ||||
AbstractTreeIterator tree = trees[i]; | AbstractTreeIterator tree = trees[i]; | ||||
if (type.isInstance(tree)) { | if (type.isInstance(tree)) { | ||||
return null; | 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 | * Inspect config and attributes to return a filtercommand applicable for | ||||
* the current path | * the current path |