diff options
Diffstat (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes')
10 files changed, 2869 insertions, 402 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeFileTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeFileTests.java new file mode 100644 index 0000000000..5d05a98d66 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeFileTests.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.attributes; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; +import org.junit.Test; + +/** + * End-to-end tests for some attribute combinations. Writes files, commit them, + * examines the index, deletes the files, performs a hard reset and checks file + * contents again. + */ +public class AttributeFileTests extends RepositoryTestCase { + + @Test + public void testTextAutoCoreEolCoreAutoCrLfInput() throws Exception { + FileBasedConfig cfg = db.getConfig(); + cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOCRLF, false); + cfg.save(); + final String content = "Line1\nLine2\n"; + try (Git git = Git.wrap(db)) { + writeTrashFile(".gitattributes", "* text=auto"); + File dummy = writeTrashFile("dummy.txt", content); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Commit with LF").call(); + assertEquals("Unexpected index state", + "[.gitattributes, mode:100644, content:* text=auto]" + + "[dummy.txt, mode:100644, content:" + content + + ']', + indexState(CONTENT)); + assertTrue("Should be able to delete " + dummy, dummy.delete()); + cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_EOL, "crlf"); + cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOCRLF, "input"); + cfg.save(); + git.reset().setMode(ResetType.HARD).call(); + assertTrue("File " + dummy + "should exist", dummy.isFile()); + String textFile = RawParseUtils.decode(IO.readFully(dummy, 512)); + assertEquals("Unexpected text content", content, textFile); + } + } + + @Test + public void testTextAutoEolLf() throws Exception { + writeTrashFile(".gitattributes", "* text=auto eol=lf"); + performTest("Test\r\nFile", "Test\nFile", "Test\nFile"); + } + + @Test + public void testTextAutoEolCrLf() throws Exception { + writeTrashFile(".gitattributes", "* text=auto eol=crlf"); + performTest("Test\r\nFile", "Test\nFile", "Test\r\nFile"); + } + + private void performTest(String initial, String index, String finalText) + throws Exception { + File dummy = writeTrashFile("dummy.foo", initial); + byte[] data = readTestResource("add.png"); + assertTrue("Expected some binary data", data.length > 100); + File binary = writeTrashFile("add.png", ""); + Files.write(binary.toPath(), data); + try (Git git = Git.wrap(db)) { + git.add().addFilepattern(".").call(); + git.commit().setMessage("test commit").call(); + // binary should be unchanged, dummy should match "index" + verifyIndexContent("dummy.foo", + index.getBytes(StandardCharsets.UTF_8)); + verifyIndexContent("add.png", data); + assertTrue("Should be able to delete " + dummy, dummy.delete()); + assertTrue("Should be able to delete " + binary, binary.delete()); + git.reset().setMode(ResetType.HARD).call(); + assertTrue("File " + dummy + " should exist", dummy.isFile()); + assertTrue("File " + binary + " should exist", binary.isFile()); + // binary should be unchanged, dummy should match "finalText" + String textFile = RawParseUtils.decode(IO.readFully(dummy, 512)); + assertEquals("Unexpected text content", finalText, textFile); + byte[] binaryFile = IO.readFully(binary, 512); + assertArrayEquals("Unexpected binary content", data, binaryFile); + } + } + + private byte[] readTestResource(String name) throws Exception { + try (InputStream in = new BufferedInputStream( + getClass().getResourceAsStream(name))) { + byte[] data = new byte[512]; + int read = in.read(data); + if (read == data.length) { + return data; + } + return Arrays.copyOf(data, read); + } + } + + private void verifyIndexContent(String path, byte[] expectedContent) + throws Exception { + DirCache dc = db.readDirCache(); + for (int i = 0; i < dc.getEntryCount(); ++i) { + DirCacheEntry entry = dc.getEntry(i); + if (path.equals(entry.getPathString())) { + byte[] data = db.open(entry.getObjectId(), Constants.OBJ_BLOB) + .getCachedBytes(); + assertArrayEquals("Unexpected index content for " + path, + expectedContent, data); + return; + } + } + fail("Path not found in index: " + path); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java index 93b954fa9a..9f9ba0de8e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeTest.java @@ -1,45 +1,12 @@ /* * Copyright (C) 2010, Marc Strapetz <marc.strapetz@syntevo.com> - * Copyright (C) 2013, Gunnar Wagenknecht - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2013, Gunnar Wagenknecht and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java new file mode 100644 index 0000000000..c41dd81add --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java @@ -0,0 +1,618 @@ +/* + * Copyright (C) 2015, 2017 Ivan Motsch <ivan.motsch@bsiag.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +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; + + @Test + public void testExpandNonMacro1() throws Exception { + setupRepo(null, null, null, "*.txt text"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("text")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testExpandNonMacro2() throws Exception { + setupRepo(null, null, null, "*.txt -text"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("-text")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testExpandNonMacro3() throws Exception { + setupRepo(null, null, null, "*.txt !text"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testExpandNonMacro4() throws Exception { + setupRepo(null, null, null, "*.txt text=auto"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("text=auto")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testExpandBuiltInMacro1() throws Exception { + setupRepo(null, null, null, "*.txt binary"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", + attrs("binary -diff -merge -text")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testExpandBuiltInMacro2() throws Exception { + setupRepo(null, null, null, "*.txt -binary"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", + attrs("-binary diff merge text")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testExpandBuiltInMacro3() throws Exception { + setupRepo(null, null, null, "*.txt !binary"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testCustomGlobalMacro1() throws Exception { + setupRepo( + "[attr]foo a -b !c d=e", null, null, "*.txt foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo a -b d=e")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testCustomGlobalMacro2() throws Exception { + setupRepo("[attr]foo a -b !c d=e", null, null, "*.txt -foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("-foo -a b d=e")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testCustomGlobalMacro3() throws Exception { + setupRepo("[attr]foo a -b !c d=e", null, null, "*.txt !foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testCustomGlobalMacro4() throws Exception { + setupRepo("[attr]foo a -b !c d=e", null, null, "*.txt foo=bar"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo=bar a -b d=bar")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testInfoOverridesGlobal() throws Exception { + setupRepo("[attr]foo bar1", + "[attr]foo bar2", null, "*.txt foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo bar2")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testWorkDirRootOverridesGlobal() throws Exception { + setupRepo("[attr]foo bar1", + null, + "[attr]foo bar3", "*.txt foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo bar3")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testInfoOverridesWorkDirRoot() throws Exception { + setupRepo("[attr]foo bar1", + "[attr]foo bar2", "[attr]foo bar3", "*.txt foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo bar2")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testRecursiveMacro() throws Exception { + setupRepo( + "[attr]foo x bar -foo", + null, null, "*.txt foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo x bar")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testCyclicMacros() throws Exception { + setupRepo( + "[attr]foo x -bar\n[attr]bar y -foo", null, null, "*.txt foo"); + + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/.gitattributes"); + assertIteration(walk, F, "sub/a.txt", attrs("foo x -bar -y")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testRelativePaths() throws Exception { + setupRepo("sub/ global", "sub/** init", + "sub/** top_sub\n*.txt top", + "sub/** subsub\nsub/ subsub2\n*.txt foo"); + // The last sub/** is in sub/.gitattributes. It must not + // apply to any of the files here. It would match for a + // further subdirectory sub/sub. The sub/ rules must match + // only for directories. + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub", attrs("global")); + assertIteration(walk, F, "sub/.gitattributes", + attrs("init top_sub")); + assertIteration(walk, F, "sub/a.txt", + attrs("init foo top top_sub")); + assertFalse("Not all files tested", walk.next()); + } + // All right, let's see that they *do* apply in sub/sub: + writeTrashFile("sub/sub/b.txt", "b"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub", attrs("global")); + assertIteration(walk, F, "sub/.gitattributes", + attrs("init top_sub")); + assertIteration(walk, F, "sub/a.txt", + attrs("init foo top top_sub")); + assertIteration(walk, D, "sub/sub", + attrs("init subsub2 top_sub global")); + assertIteration(walk, F, "sub/sub/b.txt", + attrs("init foo subsub top top_sub")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testNestedMatchNot() throws Exception { + setupRepo(null, null, "*.xml xml\n*.jar jar", null); + writeTrashFile("foo.xml/bar.jar", "b"); + writeTrashFile("foo.xml/bar.xml", "bx"); + writeTrashFile("sub/b.jar", "bj"); + writeTrashFile("sub/b.xml", "bx"); + // On foo.xml/bar.jar we must not have 'xml' + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo.xml", attrs("xml")); + assertIteration(walk, F, "foo.xml/bar.jar", attrs("jar")); + assertIteration(walk, F, "foo.xml/bar.xml", attrs("xml")); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, F, "sub/b.jar", attrs("jar")); + assertIteration(walk, F, "sub/b.xml", attrs("xml")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testNestedMatch() throws Exception { + // See also CGitAttributeTest.testNestedMatch() + setupRepo(null, null, "foo/ xml\nsub/foo/ sub\n*.jar jar", null); + writeTrashFile("foo/bar.jar", "b"); + writeTrashFile("foo/bar.xml", "bx"); + writeTrashFile("sub/b.jar", "bj"); + writeTrashFile("sub/b.xml", "bx"); + writeTrashFile("sub/foo/b.jar", "bf"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo", attrs("xml")); + assertIteration(walk, F, "foo/bar.jar", attrs("jar")); + assertIteration(walk, F, "foo/bar.xml"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, F, "sub/b.jar", attrs("jar")); + assertIteration(walk, F, "sub/b.xml"); + assertIteration(walk, D, "sub/foo", attrs("sub xml")); + assertIteration(walk, F, "sub/foo/b.jar", attrs("jar")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testNestedMatchRecursive() throws Exception { + setupRepo(null, null, "foo/** xml\n*.jar jar", null); + writeTrashFile("foo/bar.jar", "b"); + writeTrashFile("foo/bar.xml", "bx"); + writeTrashFile("sub/b.jar", "bj"); + writeTrashFile("sub/b.xml", "bx"); + writeTrashFile("sub/foo/b.jar", "bf"); + // On foo.xml/bar.jar we must not have 'xml' + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, F, "foo/bar.jar", attrs("jar xml")); + assertIteration(walk, F, "foo/bar.xml", attrs("xml")); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, F, "sub/b.jar", attrs("jar")); + assertIteration(walk, F, "sub/b.xml"); + assertIteration(walk, D, "sub/foo"); + assertIteration(walk, F, "sub/foo/b.jar", attrs("jar")); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testStarMatchOnSlashNot() throws Exception { + setupRepo(null, null, "s*xt bar", null); + writeTrashFile("sub/a.txt", "1"); + writeTrashFile("foo/sext", "2"); + writeTrashFile("foo/s.txt", "3"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, F, "foo/s.txt", attrs("bar")); + assertIteration(walk, F, "foo/sext", attrs("bar")); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testPrefixMatchNot() throws Exception { + setupRepo(null, null, "sub/new bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testComplexPathMatch() throws Exception { + setupRepo(null, null, "s[t-v]b/n[de]w bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("sub/ndw", "2"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, F, "sub/ndw", attrs("bar")); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testStarPathMatch() throws Exception { + setupRepo(null, null, "sub/new/* bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("sub/new/lower/foo.txt", "2"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new"); + assertIteration(walk, F, "sub/new/foo.txt", attrs("bar")); + assertIteration(walk, D, "sub/new/lower", attrs("bar")); + assertIteration(walk, F, "sub/new/lower/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testDirectoryMatchSubSimple() throws Exception { + setupRepo(null, null, "sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("sub/sub/new/foo.txt", "3"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, D, "foo/sub"); + assertIteration(walk, D, "foo/sub/new"); + assertIteration(walk, F, "foo/sub/new/foo.txt"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertIteration(walk, D, "sub/sub"); + assertIteration(walk, D, "sub/sub/new"); + assertIteration(walk, F, "sub/sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testDirectoryMatchSubRecursive() throws Exception { + setupRepo(null, null, "**/sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, D, "foo/sub"); + assertIteration(walk, D, "foo/sub/new", attrs("bar")); + assertIteration(walk, F, "foo/sub/new/foo.txt"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack() throws Exception { + setupRepo(null, null, "**/sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("sub/sub/new/foo.txt", "3"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, D, "foo/sub"); + assertIteration(walk, D, "foo/sub/new", attrs("bar")); + assertIteration(walk, F, "foo/sub/new/foo.txt"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertIteration(walk, D, "sub/sub"); + assertIteration(walk, D, "sub/sub/new", attrs("bar")); + assertIteration(walk, F, "sub/sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception { + setupRepo(null, null, "**/**/sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("sub/sub/new/foo.txt", "3"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, D, "foo/sub"); + assertIteration(walk, D, "foo/sub/new", attrs("bar")); + assertIteration(walk, F, "foo/sub/new/foo.txt"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertIteration(walk, D, "sub/sub"); + assertIteration(walk, D, "sub/sub/new", attrs("bar")); + assertIteration(walk, F, "sub/sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testDirectoryMatchSubComplex() throws Exception { + setupRepo(null, null, "s[uv]b/n*/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, D, "foo/sub"); + assertIteration(walk, D, "foo/sub/new"); + assertIteration(walk, F, "foo/sub/new/foo.txt"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + @Test + public void testDirectoryMatch() throws Exception { + setupRepo(null, null, "new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("foo/new", "3"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, D, "foo"); + assertIteration(walk, F, "foo/new"); + assertIteration(walk, D, "foo/sub"); + assertIteration(walk, D, "foo/sub/new", attrs("bar")); + assertIteration(walk, F, "foo/sub/new/foo.txt"); + assertIteration(walk, D, "sub"); + assertIteration(walk, F, "sub/a.txt"); + assertIteration(walk, D, "sub/new", attrs("bar")); + assertIteration(walk, F, "sub/new/foo.txt"); + assertFalse("Not all files tested", walk.next()); + } + } + + private static Collection<Attribute> attrs(String s) { + return new AttributesRule("*", s).getAttributes(); + } + + private void assertIteration(TreeWalk walk, FileMode type, String pathName) + throws IOException { + assertIteration(walk, type, pathName, + Collections.<Attribute> emptyList()); + } + + private void assertIteration(TreeWalk walk, 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.getCommonDirectory(), 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; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java index 9f82b8a1e9..6109d78184 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java @@ -1,44 +1,11 @@ /* - * Copyright (C) 2010, Red Hat Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2010, Red Hat Inc. and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -109,16 +76,16 @@ public class AttributesMatcherTest { pattern = "/src/ne?"; assertMatched(pattern, "/src/new/"); assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertNotMatched(pattern, "/src/new.c"); //Test name-only fnmatcher matches pattern = "ne?"; assertMatched(pattern, "/src/new/"); assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertMatched(pattern, "/neb"); assertNotMatched(pattern, "/src/new.c"); } @@ -169,16 +136,16 @@ public class AttributesMatcherTest { pattern = "/src/ne?"; assertMatched(pattern, "src/new/"); assertMatched(pattern, "src/new"); - assertMatched(pattern, "src/new/a.c"); - assertMatched(pattern, "src/new/a/a.c"); + assertNotMatched(pattern, "src/new/a.c"); + assertNotMatched(pattern, "src/new/a/a.c"); assertNotMatched(pattern, "src/new.c"); //Test name-only fnmatcher matches pattern = "ne?"; assertMatched(pattern, "src/new/"); assertMatched(pattern, "src/new"); - assertMatched(pattern, "src/new/a.c"); - assertMatched(pattern, "src/new/a/a.c"); + assertNotMatched(pattern, "src/new/a.c"); + assertNotMatched(pattern, "src/new/a/a.c"); assertMatched(pattern, "neb"); assertNotMatched(pattern, "src/new.c"); } @@ -197,35 +164,50 @@ public class AttributesMatcherTest { pattern = "/src/new"; assertMatched(pattern, "/src/new/"); assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertNotMatched(pattern, "/src/new.c"); //Test child directory is matched, slash after name pattern = "/src/new/"; assertMatched(pattern, "/src/new/"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertNotMatched(pattern, "/src/new"); assertNotMatched(pattern, "/src/new.c"); //Test directory is matched by name only pattern = "b1"; - assertMatched(pattern, "/src/new/a/b1/a.c"); + assertNotMatched(pattern, "/src/new/a/b1/a.c"); assertNotMatched(pattern, "/src/new/a/b2/file.c"); assertNotMatched(pattern, "/src/new/a/bb1/file.c"); assertNotMatched(pattern, "/src/new/a/file.c"); + assertNotMatched(pattern, "/src/new/a/bb1"); + assertMatched(pattern, "/src/new/a/b1"); } @Test public void testTrailingSlash() { String pattern = "/src/"; assertMatched(pattern, "/src/"); - assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/a.c"); assertNotMatched(pattern, "/src"); assertNotMatched(pattern, "/srcA/"); + + pattern = "src/"; + assertMatched(pattern, "src/"); + assertMatched(pattern, "/src/"); + assertNotMatched(pattern, "src"); + assertNotMatched(pattern, "/src/new"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "foo/src/a.c"); + assertNotMatched(pattern, "foo/src/bar/a.c"); + assertNotMatched(pattern, "foo/src/bar/src"); + assertMatched(pattern, "foo/src/"); + assertMatched(pattern, "foo/src/bar/src/"); } @Test @@ -239,51 +221,58 @@ public class AttributesMatcherTest { assertMatched(pattern, "/src/test.stp"); assertNotMatched(pattern, "/test.stp1"); assertNotMatched(pattern, "/test.astp"); + assertNotMatched(pattern, "test.stp/foo.bar"); + assertMatched(pattern, "test.stp"); + assertMatched(pattern, "test.stp/"); + assertMatched(pattern, "test.stp/test.stp"); //Test matches for name-only, applies to file name or folder name pattern = "src"; assertMatched(pattern, "/src"); assertMatched(pattern, "/src/"); - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); assertMatched(pattern, "/file/src"); //Test matches for name-only, applies only to folder names pattern = "src/"; - assertMatched(pattern, "/src/"); - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); assertNotMatched(pattern, "/src"); assertNotMatched(pattern, "/file/src"); + assertMatched(pattern, "/file/src/"); //Test matches for name-only, applies to file name or folder name //With a small wildcard pattern = "?rc"; - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); + assertMatched(pattern, "/new/src/"); assertMatched(pattern, "/file/src"); assertMatched(pattern, "/src/"); //Test matches for name-only, applies to file name or folder name //With a small wildcard pattern = "?r[a-c]"; - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); assertMatched(pattern, "/file/src"); assertMatched(pattern, "/src/"); - assertMatched(pattern, "/srb/a.c"); - assertMatched(pattern, "/grb/new/a.c"); - assertMatched(pattern, "/new/crb/a.c"); + assertNotMatched(pattern, "/srb/a.c"); + assertNotMatched(pattern, "/grb/new/a.c"); + assertNotMatched(pattern, "/new/crb/a.c"); assertMatched(pattern, "/file/3rb"); assertMatched(pattern, "/xrb/"); - assertMatched(pattern, "/3ra/a.c"); - assertMatched(pattern, "/5ra/new/a.c"); - assertMatched(pattern, "/new/1ra/a.c"); + assertNotMatched(pattern, "/3ra/a.c"); + assertNotMatched(pattern, "/5ra/new/a.c"); + assertNotMatched(pattern, "/new/1ra/a.c"); + assertNotMatched(pattern, "/new/1ra/a.c/"); assertMatched(pattern, "/file/dra"); + assertMatched(pattern, "/file/dra/"); assertMatched(pattern, "/era/"); assertNotMatched(pattern, "/crg"); assertNotMatched(pattern, "/cr3"); @@ -293,28 +282,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 +311,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 +340,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"); @@ -360,6 +349,47 @@ public class AttributesMatcherTest { assertEquals(r.getAttributes().get(2).toString(), "attribute3=value"); } + @Test + public void testBracketsInGroup() { + //combinations of brackets in brackets, escaped and not + + String[] patterns = new String[]{"[[\\]]", "[\\[\\]]"}; + for (String pattern : patterns) { + assertNotMatched(pattern, ""); + assertNotMatched(pattern, "[]"); + assertNotMatched(pattern, "]["); + assertNotMatched(pattern, "[\\[]"); + assertNotMatched(pattern, "[[]"); + assertNotMatched(pattern, "[[]]"); + assertNotMatched(pattern, "[\\[\\]]"); + + assertMatched(pattern, "["); + assertMatched(pattern, "]"); + } + + patterns = new String[]{"[[]]", "[\\[]]"}; + for (String pattern : patterns) { + assertNotMatched(pattern, ""); + assertMatched(pattern, "[]"); + assertNotMatched(pattern, "]["); + assertNotMatched(pattern, "[\\[]"); + assertNotMatched(pattern, "[[]"); + assertNotMatched(pattern, "[[]]"); + assertNotMatched(pattern, "[\\[\\]]"); + + assertNotMatched(pattern, "["); + assertNotMatched(pattern, "]"); + } + } + + @Test + public void testFileNameWithLineTerminator() { + assertMatched("a?", "a\r"); + assertMatched("a?", "dir/a\r"); + assertMatched("*a", "\ra"); + assertMatched("dir/*a*", "dir/\ra\r"); + } + /** * Check for a match. If target ends with "/", match will assume that the * target is meant to be a directory. @@ -369,7 +399,7 @@ public class AttributesMatcherTest { * @param target * Target file path relative to repository's GIT_DIR */ - public void assertMatched(String pattern, String target) { + private void assertMatched(String pattern, String target) { boolean value = match(pattern, target); assertTrue("Expected a match for: " + pattern + " with: " + target, value); @@ -384,7 +414,7 @@ public class AttributesMatcherTest { * @param target * Target file path relative to repository's GIT_DIR */ - public void assertNotMatched(String pattern, String target) { + private void assertNotMatched(String pattern, String target) { boolean value = match(pattern, target); assertFalse("Expected no match for: " + pattern + " with: " + target, value); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java index 49279e6e5a..35b953320e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java @@ -1,44 +1,11 @@ /* - * Copyright (C) 2010, Red Hat Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2010, Red Hat Inc. and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -52,21 +19,20 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.attributes.Attribute.State; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.Before; import org.junit.Test; /** - * Tests attributes node behavior on the the index. + * Tests attributes node behavior on the index. */ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { @@ -80,8 +46,6 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { private Git git; - private TreeWalk walk; - @Override @Before public void setUp() throws Exception { @@ -107,23 +71,25 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { // Adds file to index git.add().addFilepattern(".").call(); - walk = beginWalk(); - - assertIteration(F, ".gitattributes"); - assertIteration(F, "readme.txt", asList(EOL_LF)); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, F, "readme.txt", asList(EOL_LF)); - assertIteration(D, "src"); + assertIteration(walk, D, "src"); - assertIteration(D, "src/config"); - assertIteration(F, "src/config/.gitattributes"); - assertIteration(F, "src/config/readme.txt", asList(DELTA_UNSET)); - assertIteration(F, "src/config/windows.file", null); - assertIteration(F, "src/config/windows.txt", asList(DELTA_UNSET)); + assertIteration(walk, D, "src/config"); + assertIteration(walk, F, "src/config/.gitattributes"); + assertIteration(walk, F, "src/config/readme.txt", + asList(DELTA_UNSET)); + assertIteration(walk, F, "src/config/windows.file", null); + assertIteration(walk, F, "src/config/windows.txt", + asList(DELTA_UNSET)); - assertIteration(F, "windows.file", null); - assertIteration(F, "windows.txt", asList(EOL_LF)); + assertIteration(walk, F, "windows.file", null); + assertIteration(walk, F, "windows.txt", asList(EOL_LF)); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } /** @@ -140,17 +106,18 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { // Adds file to index git.add().addFilepattern(".").call(); - walk = beginWalk(); - assertIteration(F, "l0.txt"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, "l0.txt"); - assertIteration(D, "level1"); - assertIteration(F, "level1/l1.txt"); + assertIteration(walk, D, "level1"); + assertIteration(walk, F, "level1/l1.txt"); - assertIteration(D, "level1/level2"); - assertIteration(F, "level1/level2/l2.txt"); + assertIteration(walk, D, "level1/level2"); + assertIteration(walk, F, "level1/level2/l2.txt"); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } /** @@ -168,18 +135,19 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { // Adds file to index git.add().addFilepattern(".").call(); - walk = beginWalk(); - assertIteration(F, ".gitattributes"); - assertIteration(F, "l0.txt"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, F, "l0.txt"); - assertIteration(D, "level1"); - assertIteration(F, "level1/l1.txt"); + assertIteration(walk, D, "level1"); + assertIteration(walk, F, "level1/l1.txt"); - assertIteration(D, "level1/level2"); - assertIteration(F, "level1/level2/l2.txt"); + assertIteration(walk, D, "level1/level2"); + assertIteration(walk, F, "level1/level2/l2.txt"); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } @Test @@ -193,18 +161,19 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { // Adds file to index git.add().addFilepattern(".").call(); - walk = beginWalk(); - assertIteration(F, ".gitattributes"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); - assertIteration(D, "levelA"); - assertIteration(F, "levelA/.gitattributes"); - assertIteration(F, "levelA/lA.txt"); + assertIteration(walk, D, "levelA"); + assertIteration(walk, F, "levelA/.gitattributes"); + assertIteration(walk, F, "levelA/lA.txt"); - assertIteration(D, "levelB"); - assertIteration(F, "levelB/.gitattributes"); + assertIteration(walk, D, "levelB"); + assertIteration(walk, F, "levelB/.gitattributes"); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } @Test @@ -217,25 +186,27 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { // Adds file to index git.add().addFilepattern(".").call(); - walk = beginWalk(); - assertIteration(F, "gitattributes"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, "gitattributes"); - assertIteration(F, "l0.txt"); + assertIteration(walk, F, "l0.txt"); - assertIteration(D, "levelA"); - assertIteration(F, "levelA/file.gitattributes"); - assertIteration(F, "levelA/lA.txt"); + assertIteration(walk, D, "levelA"); + assertIteration(walk, F, "levelA/file.gitattributes"); + assertIteration(walk, F, "levelA/lA.txt"); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } - private void assertIteration(FileMode type, String pathName) + private void assertIteration(TreeWalk walk, FileMode type, String pathName) throws IOException { - assertIteration(type, pathName, Collections.<Attribute> emptyList()); + assertIteration(walk, type, pathName, + Collections.<Attribute> emptyList()); } - private void assertIteration(FileMode type, String pathName, + private void assertIteration(TreeWalk walk, FileMode type, String pathName, List<Attribute> nodeAttrs) throws IOException { assertTrue("walk has entry", walk.next()); assertEquals(pathName, walk.getPathString()); @@ -243,27 +214,32 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { DirCacheIterator itr = walk.getTree(0, DirCacheIterator.class); assertNotNull("has tree", itr); - AttributesNode attributeNode = itr.getEntryAttributesNode(db + AttributesNode attributesNode = itr.getEntryAttributesNode(db .newObjectReader()); - assertAttributeNode(pathName, attributeNode, nodeAttrs); + assertAttributesNode(walk, pathName, attributesNode, nodeAttrs); if (D.equals(type)) walk.enterSubtree(); } - private void assertAttributeNode(String pathName, - AttributesNode attributeNode, List<Attribute> nodeAttrs) { - if (attributeNode == null) + private void assertAttributesNode(TreeWalk walk, String pathName, + AttributesNode attributesNode, List<Attribute> nodeAttrs) + throws IOException { + if (attributesNode == null) assertTrue(nodeAttrs == null || nodeAttrs.isEmpty()); else { - Map<String, Attribute> entryAttributes = new LinkedHashMap<String, Attribute>(); - attributeNode.getAttributes(pathName, false, entryAttributes); + Attributes entryAttributes = new Attributes(); + new AttributesHandler(walk, + () -> walk.getTree(CanonicalTreeParser.class)) + .mergeAttributes(attributesNode, pathName, false, + entryAttributes); if (nodeAttrs != null && !nodeAttrs.isEmpty()) { for (Attribute attribute : nodeAttrs) { - assertThat(entryAttributes.values(), hasItem(attribute)); + assertThat(entryAttributes.getAll(), + hasItem(attribute)); } } else { assertTrue( @@ -289,8 +265,4 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { newWalk.addTree(new DirCacheIterator(db.readDirCache())); return newWalk; } - - private void endWalk() throws IOException { - assertFalse("Not all files tested", walk.next()); - } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java index ea250369a0..dbbcb75da9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributeNodeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java @@ -1,47 +1,15 @@ /* - * Copyright (C) 2014, Obeo. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2014, Obeo. and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.attributes.Attribute.State.SET; import static org.eclipse.jgit.attributes.Attribute.State.UNSET; import static org.junit.Assert.assertEquals; @@ -49,18 +17,20 @@ import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.After; import org.junit.Test; /** * Test {@link AttributesNode} */ -public class AttributeNodeTest { +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); @@ -87,7 +57,7 @@ public class AttributeNodeTest { String attributeFileContent = "*.type1 A -B C=value\n" + "*.type2 -A B C=value2"; - is = new ByteArrayInputStream(attributeFileContent.getBytes()); + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); AttributesNode node = new AttributesNode(); node.parse(is); assertAttribute("file.type1", node, @@ -101,21 +71,21 @@ public class AttributeNodeTest { String attributeFileContent = "!*.type1 A -B C=value\n" + "!*.type2 -A B C=value2"; - is = new ByteArrayInputStream(attributeFileContent.getBytes()); + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); AttributesNode node = new AttributesNode(); node.parse(is); - assertAttribute("file.type1", node, Collections.<Attribute> emptySet()); - assertAttribute("file.type2", node, Collections.<Attribute> emptySet()); + assertAttribute("file.type1", node, new Attributes()); + assertAttribute("file.type2", node, new Attributes()); } @Test public void testEmptyNegativeAttributeKey() throws IOException { String attributeFileContent = "*.type1 - \n" // + "*.type2 - -A"; - is = new ByteArrayInputStream(attributeFileContent.getBytes()); + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); AttributesNode node = new AttributesNode(); node.parse(is); - assertAttribute("file.type1", node, Collections.<Attribute> emptySet()); + assertAttribute("file.type1", node, new Attributes()); assertAttribute("file.type2", node, asSet(A_UNSET_ATTR)); } @@ -124,11 +94,11 @@ public class AttributeNodeTest { String attributeFileContent = "*.type1 = \n" // + "*.type2 =value\n"// + "*.type3 attr=\n"; - is = new ByteArrayInputStream(attributeFileContent.getBytes()); + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); AttributesNode node = new AttributesNode(); node.parse(is); - assertAttribute("file.type1", node, Collections.<Attribute> emptySet()); - assertAttribute("file.type2", node, Collections.<Attribute> emptySet()); + assertAttribute("file.type1", node, new Attributes()); + assertAttribute("file.type2", node, new Attributes()); assertAttribute("file.type3", node, asSet(new Attribute("attr", ""))); } @@ -139,7 +109,7 @@ public class AttributeNodeTest { + " \n" // + "*.type2 -A B C=value2"; - is = new ByteArrayInputStream(attributeFileContent.getBytes()); + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); AttributesNode node = new AttributesNode(); node.parse(is); assertAttribute("file.type1", node, @@ -155,7 +125,7 @@ public class AttributeNodeTest { + "*.type3 \t\t B\n" // + "*.type3\t-A";// - is = new ByteArrayInputStream(attributeFileContent.getBytes()); + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); AttributesNode node = new AttributesNode(); node.parse(is); assertAttribute("file.type1", node, @@ -165,18 +135,36 @@ public class AttributeNodeTest { assertAttribute("file.type3", node, asSet(A_UNSET_ATTR, B_SET_ATTR)); } + @Test + public void testDoubleAsteriskAtEnd() throws IOException { + String attributeFileContent = "dir/** \tA -B\tC=value"; + + is = new ByteArrayInputStream(attributeFileContent.getBytes(UTF_8)); + AttributesNode node = new AttributesNode(); + node.parse(is); + assertAttribute("dir", node, + asSet(new Attribute[]{})); + assertAttribute("dir/", node, + asSet(new Attribute[]{})); + assertAttribute("dir/file.type1", node, + asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR)); + assertAttribute("dir/sub/", node, + asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR)); + assertAttribute("dir/sub/file.type1", node, + asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR)); + } + private void assertAttribute(String path, AttributesNode node, - Set<Attribute> attrs) { - HashMap<String, Attribute> attributes = new HashMap<String, Attribute>(); - node.getAttributes(path, false, attributes); - assertEquals(attrs, new HashSet<Attribute>(attributes.values())); + Attributes attrs) throws IOException { + Attributes attributes = new Attributes(); + new AttributesHandler(DUMMY_WALK, + () -> DUMMY_WALK.getTree(CanonicalTreeParser.class)) + .mergeAttributes(node, path, false, attributes); + assertEquals(attrs, attributes); } - static Set<Attribute> asSet(Attribute... attrs) { - Set<Attribute> result = new HashSet<Attribute>(); - for (Attribute attr : attrs) - result.add(attr); - return result; + static Attributes asSet(Attribute... attrs) { + return new Attributes(attrs); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java index 64b0535d6a..c6c91386a2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java @@ -1,44 +1,11 @@ /* - * Copyright (C) 2014, Obeo. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2014, Obeo. and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -53,15 +20,13 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import org.eclipse.jgit.attributes.Attribute.State; -import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; @@ -76,16 +41,10 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { private static final FileMode F = FileMode.REGULAR_FILE; - private static Attribute EOL_CRLF = new Attribute("eol", "crlf"); - private static Attribute EOL_LF = new Attribute("eol", "lf"); private static Attribute DELTA_UNSET = new Attribute("delta", State.UNSET); - private static Attribute CUSTOM_VALUE = new Attribute("custom", "value"); - - private TreeWalk walk; - @Test public void testRules() throws Exception { @@ -109,30 +68,26 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { writeTrashFile("src/config/windows.file", ""); writeTrashFile("src/config/windows.txt", ""); - walk = beginWalk(); - - assertIteration(F, ".gitattributes"); - assertIteration(F, "global.txt", asList(EOL_LF), null, - asList(CUSTOM_VALUE)); - assertIteration(F, "readme.txt", asList(EOL_LF), null, - asList(CUSTOM_VALUE)); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, F, "global.txt", asList(EOL_LF)); + assertIteration(walk, F, "readme.txt", asList(EOL_LF)); - assertIteration(D, "src"); + assertIteration(walk, D, "src"); - assertIteration(D, "src/config"); - assertIteration(F, "src/config/.gitattributes"); - assertIteration(F, "src/config/readme.txt", asList(DELTA_UNSET), null, - asList(CUSTOM_VALUE)); - assertIteration(F, "src/config/windows.file", null, asList(EOL_CRLF), - null); - assertIteration(F, "src/config/windows.txt", asList(DELTA_UNSET), - asList(EOL_CRLF), asList(CUSTOM_VALUE)); + assertIteration(walk, D, "src/config"); + assertIteration(walk, F, "src/config/.gitattributes"); + assertIteration(walk, F, "src/config/readme.txt", + asList(DELTA_UNSET)); + assertIteration(walk, F, "src/config/windows.file", null); + assertIteration(walk, F, "src/config/windows.txt", + asList(DELTA_UNSET)); - assertIteration(F, "windows.file", null, asList(EOL_CRLF), null); - assertIteration(F, "windows.txt", asList(EOL_LF), asList(EOL_CRLF), - asList(CUSTOM_VALUE)); + assertIteration(walk, F, "windows.file", null); + assertIteration(walk, F, "windows.txt", asList(EOL_LF)); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } /** @@ -147,17 +102,17 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { writeTrashFile("level1/l1.txt", ""); writeTrashFile("level1/level2/l2.txt", ""); - walk = beginWalk(); - - assertIteration(F, "l0.txt"); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, "l0.txt"); - assertIteration(D, "level1"); - assertIteration(F, "level1/l1.txt"); + assertIteration(walk, D, "level1"); + assertIteration(walk, F, "level1/l1.txt"); - assertIteration(D, "level1/level2"); - assertIteration(F, "level1/level2/l2.txt"); + assertIteration(walk, D, "level1/level2"); + assertIteration(walk, F, "level1/level2/l2.txt"); - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } /** @@ -173,18 +128,18 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { writeTrashFile("level1/l1.txt", ""); writeTrashFile("level1/level2/l2.txt", ""); - walk = beginWalk(); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); + assertIteration(walk, F, "l0.txt"); - assertIteration(F, ".gitattributes"); - assertIteration(F, "l0.txt"); + assertIteration(walk, D, "level1"); + assertIteration(walk, F, "level1/l1.txt"); - assertIteration(D, "level1"); - assertIteration(F, "level1/l1.txt"); + assertIteration(walk, D, "level1/level2"); + assertIteration(walk, F, "level1/level2/l2.txt"); - assertIteration(D, "level1/level2"); - assertIteration(F, "level1/level2/l2.txt"); - - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } @Test @@ -196,30 +151,28 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { writeTrashFile("levelA/lA.txt", ""); - walk = beginWalk(); + try (TreeWalk walk = beginWalk()) { + assertIteration(walk, F, ".gitattributes"); - assertIteration(F, ".gitattributes"); + assertIteration(walk, D, "levelA"); + assertIteration(walk, F, "levelA/.gitattributes"); + assertIteration(walk, F, "levelA/lA.txt"); - assertIteration(D, "levelA"); - assertIteration(F, "levelA/.gitattributes"); - assertIteration(F, "levelA/lA.txt"); + assertIteration(walk, D, "levelB"); + assertIteration(walk, F, "levelB/.gitattributes"); - assertIteration(D, "levelB"); - assertIteration(F, "levelB/.gitattributes"); - - endWalk(); + assertFalse("Not all files tested", walk.next()); + } } - private void assertIteration(FileMode type, String pathName) + private void assertIteration(TreeWalk walk, FileMode type, String pathName) throws IOException { - assertIteration(type, pathName, Collections.<Attribute> emptyList(), - Collections.<Attribute> emptyList(), + assertIteration(walk, type, pathName, Collections.<Attribute> emptyList()); } - private void assertIteration(FileMode type, String pathName, - List<Attribute> nodeAttrs, List<Attribute> infoAttrs, - List<Attribute> globalAttrs) + private void assertIteration(TreeWalk walk, FileMode type, String pathName, + List<Attribute> nodeAttrs) throws IOException { assertTrue("walk has entry", walk.next()); assertEquals(pathName, walk.getPathString()); @@ -227,29 +180,30 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { WorkingTreeIterator itr = walk.getTree(0, WorkingTreeIterator.class); assertNotNull("has tree", itr); - AttributesNode attributeNode = itr.getEntryAttributesNode(); - assertAttributeNode(pathName, attributeNode, nodeAttrs); - AttributesNode infoAttributeNode = itr.getInfoAttributesNode(); - assertAttributeNode(pathName, infoAttributeNode, infoAttrs); - AttributesNode globalAttributeNode = itr.getGlobalAttributesNode(); - assertAttributeNode(pathName, globalAttributeNode, globalAttrs); + AttributesNode attributesNode = itr.getEntryAttributesNode(); + assertAttributesNode(walk, pathName, attributesNode, nodeAttrs); if (D.equals(type)) walk.enterSubtree(); } - private void assertAttributeNode(String pathName, - AttributesNode attributeNode, List<Attribute> nodeAttrs) { - if (attributeNode == null) + private void assertAttributesNode(TreeWalk walk, String pathName, + AttributesNode attributesNode, List<Attribute> nodeAttrs) + throws IOException { + if (attributesNode == null) assertTrue(nodeAttrs == null || nodeAttrs.isEmpty()); else { - Map<String, Attribute> entryAttributes = new LinkedHashMap<String, Attribute>(); - attributeNode.getAttributes(pathName, false, entryAttributes); + Attributes entryAttributes = new Attributes(); + new AttributesHandler(walk, + () -> walk.getTree(CanonicalTreeParser.class)) + .mergeAttributes(attributesNode, pathName, false, + entryAttributes); if (nodeAttrs != null && !nodeAttrs.isEmpty()) { for (Attribute attribute : nodeAttrs) { - assertThat(entryAttributes.values(), hasItem(attribute)); + assertThat(entryAttributes.getAll(), + hasItem(attribute)); } } else { assertTrue( @@ -270,13 +224,9 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { writeTrashFile(name, data.toString()); } - private TreeWalk beginWalk() throws CorruptObjectException { + private TreeWalk beginWalk() { TreeWalk newWalk = new TreeWalk(db); newWalk.addTree(new FileTreeIterator(db)); return newWalk; } - - private void endWalk() throws IOException { - assertFalse("Not all files tested", walk.next()); - } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java new file mode 100644 index 0000000000..562a515721 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.attributes; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests that verify that the attributes of files in a repository are the same + * in JGit and in C-git. + */ +public class CGitAttributesTest extends RepositoryTestCase { + + @Before + public void initRepo() throws IOException { + // Because we run C-git, we must ensure that global or user exclude + // files cannot influence the tests. So we set core.excludesFile to an + // empty file inside the repository. + StoredConfig config = db.getConfig(); + File fakeUserGitignore = writeTrashFile(".fake_user_gitignore", ""); + config.setString("core", null, "excludesFile", + fakeUserGitignore.getAbsolutePath()); + // Disable case-insensitivity -- JGit doesn't handle that yet. + config.setBoolean("core", null, "ignoreCase", false); + // And try to switch off the global attributes file, too. + config.setString("core", null, "attributesFile", + fakeUserGitignore.getAbsolutePath()); + config.save(); + } + + private void createFiles(String... paths) throws IOException { + for (String path : paths) { + writeTrashFile(path, "x"); + } + } + + private String toString(TemporaryBuffer b) throws IOException { + return RawParseUtils.decode(b.toByteArray()); + } + + private Attribute fromString(String key, String value) { + if ("set".equals(value)) { + return new Attribute(key, Attribute.State.SET); + } + if ("unset".equals(value)) { + return new Attribute(key, Attribute.State.UNSET); + } + if ("unspecified".equals(value)) { + return new Attribute(key, Attribute.State.UNSPECIFIED); + } + return new Attribute(key, value); + } + + private LinkedHashMap<String, Attributes> cgitAttributes( + Set<String> allFiles) throws Exception { + FS fs = db.getFS(); + StringBuilder input = new StringBuilder(); + for (String filename : allFiles) { + input.append(filename).append('\n'); + } + ProcessBuilder builder = fs.runInShell("git", + new String[] { "check-attr", "--stdin", "--all" }); + builder.directory(db.getWorkTree()); + builder.environment().put("HOME", fs.userHome().getAbsolutePath()); + ExecutionResult result = fs.execute(builder, new ByteArrayInputStream( + input.toString().getBytes(UTF_8))); + String errorOut = toString(result.getStderr()); + assertEquals("External git failed", "exit 0\n", + "exit " + result.getRc() + '\n' + errorOut); + LinkedHashMap<String, Attributes> map = new LinkedHashMap<>(); + try (BufferedReader r = new BufferedReader(new InputStreamReader( + new BufferedInputStream(result.getStdout().openInputStream()), + UTF_8))) { + r.lines().forEach(line -> { + // Parse the line and add to result map + int i = line.indexOf(':'); + String path = line.substring(0, i).trim(); + int start = i + 1; + i = line.indexOf(':', start); + String key = line.substring(start, i).trim(); + String value = line.substring(i + 1).trim(); + Attribute attr = fromString(key, value); + Attributes attrs = map.get(path); + if (attrs == null) { + attrs = new Attributes(attr); + map.put(path, attrs); + } else { + attrs.put(attr); + } + }); + } + return map; + } + + private LinkedHashMap<String, Attributes> jgitAttributes() + throws IOException { + // Do a tree walk and return a list of all files and directories with + // their attributes + LinkedHashMap<String, Attributes> result = new LinkedHashMap<>(); + try (TreeWalk walk = new TreeWalk(db)) { + walk.addTree(new FileTreeIterator(db)); + walk.setFilter(new NotIgnoredFilter(0)); + while (walk.next()) { + String path = walk.getPathString(); + if (walk.isSubtree() && !path.endsWith("/")) { + // git check-attr expects directory paths to end with a + // slash + path += '/'; + } + Attributes attrs = walk.getAttributes(); + if (attrs != null && !attrs.isEmpty()) { + result.put(path, attrs); + } else { + result.put(path, null); + } + if (walk.isSubtree()) { + walk.enterSubtree(); + } + } + } + return result; + } + + private void assertSameAsCGit() throws Exception { + LinkedHashMap<String, Attributes> jgit = jgitAttributes(); + LinkedHashMap<String, Attributes> cgit = cgitAttributes(jgit.keySet()); + // remove all without attributes + Iterator<Map.Entry<String, Attributes>> iterator = jgit.entrySet() + .iterator(); + while (iterator.hasNext()) { + Map.Entry<String, Attributes> entry = iterator.next(); + if (entry.getValue() == null) { + iterator.remove(); + } + } + assertArrayEquals("JGit attributes differ from C git", + cgit.entrySet().toArray(), jgit.entrySet().toArray()); + } + + @Test + public void testBug508568() throws Exception { + createFiles("foo.xml/bar.jar", "sub/foo.xml/bar.jar"); + writeTrashFile(".gitattributes", "*.xml xml\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testRelativePath() throws Exception { + createFiles("sub/foo.txt"); + writeTrashFile("sub/.gitattributes", "sub/** sub\n" + "*.txt txt\n"); + assertSameAsCGit(); + } + + @Test + public void testRelativePaths() throws Exception { + createFiles("sub/foo.txt", "sub/sub/bar", "foo/sub/a.txt", + "foo/sub/bar/a.tmp"); + writeTrashFile(".gitattributes", "sub/** sub\n" + "*.txt txt\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatchNot() throws Exception { + createFiles("foo.xml/bar.jar", "foo.xml/bar.xml", "sub/b.jar", + "sub/b.xml"); + writeTrashFile("sub/.gitattributes", "*.xml xml\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatch() throws Exception { + // This is an interesting test. At the time of this writing, the + // gitignore documentation says: "In other words, foo/ will match a + // directory foo AND PATHS UNDERNEATH IT, but will not match a regular + // file or a symbolic link foo". (Emphasis added.) And gitattributes is + // supposed to follow the same rules. But the documentation appears to + // lie: C-git will *not* apply the attribute "xml" to *any* files in + // any subfolder "foo" here. It will only apply the "jar" attribute + // to the three *.jar files. + // + // The point is probably that ignores are handled top-down, and once a + // directory "foo" is matched (here: on paths "foo" and "sub/foo" by + // pattern "foo/"), the directory is excluded and the gitignore + // documentation also says: "It is not possible to re-include a file if + // a parent directory of that file is excluded." So once the pattern + // "foo/" has matched, it appears as if everything beneath would also be + // matched. + // + // But not so for gitattributes! The foo/ rule only matches the + // directory itself, but not anything beneath. + createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml", + "sub/foo/b.jar"); + writeTrashFile(".gitattributes", + "foo/ xml\n" + "sub/foo/ sub\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatchWithWildcard() throws Exception { + // See above. + createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml", + "sub/foo/b.jar"); + writeTrashFile(".gitattributes", + "**/foo/ xml\n" + "*/foo/ sub\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatchRecursive() throws Exception { + createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml", + "sub/foo/b.jar"); + writeTrashFile(".gitattributes", "foo/** xml\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testStarMatchOnSlashNot() throws Exception { + createFiles("sub/a.txt", "foo/sext", "foo/s.txt"); + writeTrashFile(".gitattributes", "s*xt bar"); + assertSameAsCGit(); + } + + @Test + public void testPrefixMatchNot() throws Exception { + createFiles("src/new/foo.txt"); + writeTrashFile(".gitattributes", "src/new bar\n"); + assertSameAsCGit(); + } + + @Test + public void testComplexPathMatchNot() throws Exception { + createFiles("src/new/foo.txt", "src/ndw"); + writeTrashFile(".gitattributes", "s[p-s]c/n[de]w bar\n"); + assertSameAsCGit(); + } + + @Test + public void testStarPathMatchNot() throws Exception { + createFiles("src/new/foo.txt", "src/ndw"); + writeTrashFile(".gitattributes", "src/* bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubSimple() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursive() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack() throws Exception { + createFiles("src/new/foo.txt", "src/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception { + createFiles("src/new/foo.txt", "src/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack3() throws Exception { + createFiles("src/new/src/new/foo.txt", + "foo/src/new/bar/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack4() throws Exception { + createFiles("src/src/src/new/foo.txt", + "foo/src/src/bar/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/src/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack5() throws Exception { + createFiles("x/a/a/b/foo.txt", "x/y/z/b/a/b/foo.txt", + "x/y/a/a/a/a/b/foo.txt", "x/y/a/a/a/a/b/a/b/foo.txt"); + writeTrashFile(".gitattributes", "**/*/a/b bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack6() throws Exception { + createFiles("x/a/a/b/foo.txt", "x/y/a/b/a/b/foo.txt"); + writeTrashFile(".gitattributes", "**/*/**/a/b bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryWildmatchDoesNotMatchFiles1() throws Exception { + createFiles("a", "dir/b", "dir/sub/c"); + writeTrashFile(".gitattributes", "**/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryWildmatchDoesNotMatchFiles2() throws Exception { + createFiles("a", "dir/b", "dir/sub/c"); + writeTrashFile(".gitattributes", "**/**/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryWildmatchDoesNotMatchFiles3() throws Exception { + createFiles("a", "x/b", "sub/x/c", "sub/x/d/e"); + writeTrashFile(".gitattributes", "x/**/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryWildmatchDoesNotMatchFiles4() throws Exception { + createFiles("a", "dir/x", "dir/sub1/x", "dir/sub2/x/y"); + writeTrashFile(".gitattributes", "x/**/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubComplex() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "s[rs]c/n*/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatch() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testBracketsInGroup() throws Exception { + createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]"); + writeTrashFile(".gitattributes", "[[]] bar1\n" + "[\\[]] bar2\n" + + "[[\\]] bar3\n" + "[\\[\\]] bar4\n"); + assertSameAsCGit(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java new file mode 100644 index 0000000000..698fdb31a8 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java @@ -0,0 +1,839 @@ +/* + * Copyright (C) 2014, Obeo. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +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.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.attributes.Attribute.State; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.TreeWalk.OperationType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests the attributes are correctly computed in a {@link TreeWalk}. + * + * @see TreeWalk#getAttributes() + */ +public class TreeWalkAttributeTest extends RepositoryTestCase { + + private static final FileMode M = FileMode.MISSING; + + private static final FileMode D = FileMode.TREE; + + private static final FileMode F = FileMode.REGULAR_FILE; + + private static Attribute EOL_CRLF = new Attribute("eol", "crlf"); + + private static Attribute EOL_LF = new Attribute("eol", "lf"); + + private static Attribute TEXT_SET = new Attribute("text", State.SET); + + private static Attribute TEXT_UNSET = new Attribute("text", State.UNSET); + + private static Attribute DELTA_UNSET = new Attribute("delta", State.UNSET); + + private static Attribute DELTA_SET = new Attribute("delta", State.SET); + + private static Attribute CUSTOM_GLOBAL = new Attribute("custom", "global"); + + private static Attribute CUSTOM_INFO = new Attribute("custom", "info"); + + private static Attribute CUSTOM_ROOT = new Attribute("custom", "root"); + + private static Attribute CUSTOM_PARENT = new Attribute("custom", "parent"); + + private static Attribute CUSTOM_CURRENT = new Attribute("custom", "current"); + + private static Attribute CUSTOM2_UNSET = new Attribute("custom2", + State.UNSET); + + private static Attribute CUSTOM2_SET = new Attribute("custom2", State.SET); + + private TreeWalk walk; + + private TreeWalk ci_walk; + + private Git git; + + private File customAttributeFile; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + } + + @Override + @After + public void tearDown() throws Exception { + if (walk != null) { + walk.close(); + } + if (ci_walk != null) { + ci_walk.close(); + } + super.tearDown(); + if (customAttributeFile != null) + customAttributeFile.delete(); + } + + /** + * Checks that the attributes are computed correctly depending on the + * operation type. + * <p> + * In this test we changed the content of the attribute files in the working + * tree compared to the one in the index. + * </p> + * + * @throws IOException + * @throws NoFilepatternException + * @throws GitAPIException + */ + @Test + public void testCheckinCheckoutDifferences() throws IOException, + NoFilepatternException, GitAPIException { + + writeGlobalAttributeFile("globalAttributesFile", "*.txt -custom2"); + writeAttributesFile(".git/info/attributes", "*.txt eol=crlf"); + writeAttributesFile(".gitattributes", "*.txt custom=root"); + writeAttributesFile("level1/.gitattributes", "*.txt text"); + writeAttributesFile("level1/level2/.gitattributes", "*.txt -delta"); + + writeTrashFile("l0.txt", ""); + + writeTrashFile("level1/l1.txt", ""); + + writeTrashFile("level1/level2/l2.txt", ""); + + git.add().addFilepattern(".").call(); + + beginWalk(); + + // Modify all attributes + writeGlobalAttributeFile("globalAttributesFile", "*.txt custom2"); + writeAttributesFile(".git/info/attributes", "*.txt eol=lf"); + writeAttributesFile(".gitattributes", "*.txt custom=info"); + writeAttributesFile("level1/.gitattributes", "*.txt -text"); + writeAttributesFile("level1/level2/.gitattributes", "*.txt delta"); + + assertEntry(F, ".gitattributes"); + assertEntry(F, "l0.txt", asSet(EOL_LF, CUSTOM_INFO, CUSTOM2_SET), + asSet(EOL_LF, CUSTOM_ROOT, CUSTOM2_SET)); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + assertEntry(F, "level1/l1.txt", + asSet(EOL_LF, CUSTOM_INFO, CUSTOM2_SET, TEXT_UNSET), + asSet(EOL_LF, CUSTOM_ROOT, CUSTOM2_SET, TEXT_SET)); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/.gitattributes"); + assertEntry(F, "level1/level2/l2.txt", + asSet(EOL_LF, CUSTOM_INFO, CUSTOM2_SET, TEXT_UNSET, DELTA_SET), + asSet(EOL_LF, CUSTOM_ROOT, CUSTOM2_SET, TEXT_SET, DELTA_UNSET)); + + endWalk(); + } + + /** + * Checks that the index is used as fallback when the git attributes file + * are missing in the working tree. + * + * @throws IOException + * @throws NoFilepatternException + * @throws GitAPIException + */ + @Test + public void testIndexOnly() throws IOException, NoFilepatternException, + GitAPIException { + List<File> attrFiles = new ArrayList<>(); + attrFiles.add(writeGlobalAttributeFile("globalAttributesFile", + "*.txt -custom2")); + attrFiles.add(writeAttributesFile(".git/info/attributes", + "*.txt eol=crlf")); + attrFiles + .add(writeAttributesFile(".gitattributes", "*.txt custom=root")); + attrFiles + .add(writeAttributesFile("level1/.gitattributes", "*.txt text")); + attrFiles.add(writeAttributesFile("level1/level2/.gitattributes", + "*.txt -delta")); + + writeTrashFile("l0.txt", ""); + + writeTrashFile("level1/l1.txt", ""); + + writeTrashFile("level1/level2/l2.txt", ""); + + git.add().addFilepattern(".").call(); + + // Modify all attributes + for (File attrFile : attrFiles) + attrFile.delete(); + + beginWalk(); + + assertEntry(M, ".gitattributes"); + assertEntry(F, "l0.txt", asSet(CUSTOM_ROOT)); + + assertEntry(D, "level1"); + assertEntry(M, "level1/.gitattributes"); + assertEntry(F, "level1/l1.txt", + + asSet(CUSTOM_ROOT, TEXT_SET)); + + assertEntry(D, "level1/level2"); + assertEntry(M, "level1/level2/.gitattributes"); + assertEntry(F, "level1/level2/l2.txt", + + asSet(CUSTOM_ROOT, TEXT_SET, DELTA_UNSET)); + + endWalk(); + } + + /** + * Check that we search in the working tree for attributes although the file + * we are currently inspecting does not exist anymore in the working tree. + * + * @throws IOException + * @throws NoFilepatternException + * @throws GitAPIException + */ + @Test + public void testIndexOnly2() + throws IOException, NoFilepatternException, GitAPIException { + File l2 = writeTrashFile("level1/level2/l2.txt", ""); + writeTrashFile("level1/level2/l1.txt", ""); + + git.add().addFilepattern(".").call(); + + writeAttributesFile(".gitattributes", "*.txt custom=root"); + assertTrue(l2.delete()); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + assertEntry(D, "level1"); + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/l1.txt", asSet(CUSTOM_ROOT)); + assertEntry(M, "level1/level2/l2.txt", asSet(CUSTOM_ROOT)); + + endWalk(); + } + + /** + * Basic test for git attributes. + * <p> + * In this use case files are present in both the working tree and the index + * </p> + * + * @throws IOException + * @throws NoFilepatternException + * @throws GitAPIException + */ + @Test + public void testRules() throws IOException, NoFilepatternException, + GitAPIException { + writeAttributesFile(".git/info/attributes", "windows* eol=crlf"); + + writeAttributesFile(".gitattributes", "*.txt eol=lf"); + writeTrashFile("windows.file", ""); + writeTrashFile("windows.txt", ""); + writeTrashFile("readme.txt", ""); + + writeAttributesFile("src/config/.gitattributes", "*.txt -delta"); + writeTrashFile("src/config/readme.txt", ""); + writeTrashFile("src/config/windows.file", ""); + writeTrashFile("src/config/windows.txt", ""); + + beginWalk(); + + git.add().addFilepattern(".").call(); + + assertEntry(F, ".gitattributes"); + assertEntry(F, "readme.txt", asSet(EOL_LF)); + + assertEntry(D, "src"); + assertEntry(D, "src/config"); + assertEntry(F, "src/config/.gitattributes"); + assertEntry(F, "src/config/readme.txt", asSet(DELTA_UNSET, EOL_LF)); + assertEntry(F, "src/config/windows.file", asSet(EOL_CRLF)); + assertEntry(F, "src/config/windows.txt", asSet(DELTA_UNSET, EOL_CRLF)); + + assertEntry(F, "windows.file", asSet(EOL_CRLF)); + assertEntry(F, "windows.txt", asSet(EOL_CRLF)); + + endWalk(); + } + + /** + * Checks that if there is no .gitattributes file in the repository + * everything still work fine. + * + * @throws IOException + */ + @Test + public void testNoAttributes() throws IOException { + writeTrashFile("l0.txt", ""); + writeTrashFile("level1/l1.txt", ""); + writeTrashFile("level1/level2/l2.txt", ""); + + beginWalk(); + + assertEntry(F, "l0.txt"); + + assertEntry(D, "level1"); + assertEntry(F, "level1/l1.txt"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/l2.txt"); + + endWalk(); + } + + /** + * Checks that an empty .gitattribute file does not return incorrect value. + * + * @throws IOException + */ + @Test + public void testEmptyGitAttributeFile() throws IOException { + writeAttributesFile(".git/info/attributes", ""); + writeTrashFile("l0.txt", ""); + writeAttributesFile(".gitattributes", ""); + writeTrashFile("level1/l1.txt", ""); + writeTrashFile("level1/level2/l2.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + assertEntry(F, "l0.txt"); + + assertEntry(D, "level1"); + assertEntry(F, "level1/l1.txt"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/l2.txt"); + + endWalk(); + } + + @Test + public void testNoMatchingAttributes() throws IOException { + writeAttributesFile(".git/info/attributes", "*.java delta"); + writeAttributesFile(".gitattributes", "*.java -delta"); + writeAttributesFile("levelA/.gitattributes", "*.java eol=lf"); + writeAttributesFile("levelB/.gitattributes", "*.txt eol=lf"); + + writeTrashFile("levelA/lA.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + + assertEntry(D, "levelA"); + assertEntry(F, "levelA/.gitattributes"); + assertEntry(F, "levelA/lA.txt"); + + assertEntry(D, "levelB"); + assertEntry(F, "levelB/.gitattributes"); + + endWalk(); + } + + /** + * Checks that $GIT_DIR/info/attributes file has the highest precedence. + * + * @throws IOException + */ + @Test + public void testPrecedenceInfo() throws IOException { + writeGlobalAttributeFile("globalAttributesFile", "*.txt custom=global"); + writeAttributesFile(".git/info/attributes", "*.txt custom=info"); + writeAttributesFile(".gitattributes", "*.txt custom=root"); + writeAttributesFile("level1/.gitattributes", "*.txt custom=parent"); + writeAttributesFile("level1/level2/.gitattributes", + "*.txt custom=current"); + + writeTrashFile("level1/level2/file.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/.gitattributes"); + assertEntry(F, "level1/level2/file.txt", asSet(CUSTOM_INFO)); + + endWalk(); + } + + /** + * Checks that a subfolder ".gitattributes" file has precedence over its + * parent. + * + * @throws IOException + */ + @Test + public void testPrecedenceCurrent() throws IOException { + writeGlobalAttributeFile("globalAttributesFile", "*.txt custom=global"); + writeAttributesFile(".gitattributes", "*.txt custom=root"); + writeAttributesFile("level1/.gitattributes", "*.txt custom=parent"); + writeAttributesFile("level1/level2/.gitattributes", + "*.txt custom=current"); + + writeTrashFile("level1/level2/file.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/.gitattributes"); + assertEntry(F, "level1/level2/file.txt", asSet(CUSTOM_CURRENT)); + + endWalk(); + } + + /** + * Checks that the parent ".gitattributes" file is used as fallback. + * + * @throws IOException + */ + @Test + public void testPrecedenceParent() throws IOException { + writeGlobalAttributeFile("globalAttributesFile", "*.txt custom=global"); + writeAttributesFile(".gitattributes", "*.txt custom=root"); + writeAttributesFile("level1/.gitattributes", "*.txt custom=parent"); + + writeTrashFile("level1/level2/file.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/file.txt", asSet(CUSTOM_PARENT)); + + endWalk(); + } + + /** + * Checks that the grand parent ".gitattributes" file is used as fallback. + * + * @throws IOException + */ + @Test + public void testPrecedenceRoot() throws IOException { + writeGlobalAttributeFile("globalAttributesFile", "*.txt custom=global"); + writeAttributesFile(".gitattributes", "*.txt custom=root"); + + writeTrashFile("level1/level2/file.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + + assertEntry(D, "level1"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/file.txt", asSet(CUSTOM_ROOT)); + + endWalk(); + } + + /** + * Checks that the global attribute file is used as fallback. + * + * @throws IOException + */ + @Test + public void testPrecedenceGlobal() throws IOException { + writeGlobalAttributeFile("globalAttributesFile", "*.txt custom=global"); + + writeTrashFile("level1/level2/file.txt", ""); + + beginWalk(); + + assertEntry(D, "level1"); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/file.txt", asSet(CUSTOM_GLOBAL)); + + endWalk(); + } + + /** + * Checks the precedence on a hierarchy with multiple attributes. + * <p> + * In this test all file are present in both the working tree and the index. + * </p> + * + * @throws IOException + * @throws GitAPIException + * @throws NoFilepatternException + */ + @Test + public void testHierarchyBothIterator() throws IOException, + NoFilepatternException, GitAPIException { + writeAttributesFile(".git/info/attributes", "*.global eol=crlf"); + writeAttributesFile(".gitattributes", "*.local eol=lf"); + writeAttributesFile("level1/.gitattributes", "*.local text"); + writeAttributesFile("level1/level2/.gitattributes", "*.local -text"); + + writeTrashFile("l0.global", ""); + writeTrashFile("l0.local", ""); + + writeTrashFile("level1/l1.global", ""); + writeTrashFile("level1/l1.local", ""); + + writeTrashFile("level1/level2/l2.global", ""); + writeTrashFile("level1/level2/l2.local", ""); + + beginWalk(); + + git.add().addFilepattern(".").call(); + + assertEntry(F, ".gitattributes"); + assertEntry(F, "l0.global", asSet(EOL_CRLF)); + assertEntry(F, "l0.local", asSet(EOL_LF)); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + assertEntry(F, "level1/l1.global", asSet(EOL_CRLF)); + assertEntry(F, "level1/l1.local", asSet(EOL_LF, TEXT_SET)); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/.gitattributes"); + assertEntry(F, "level1/level2/l2.global", asSet(EOL_CRLF)); + assertEntry(F, "level1/level2/l2.local", asSet(EOL_LF, TEXT_UNSET)); + + endWalk(); + + } + + /** + * Checks the precedence on a hierarchy with multiple attributes. + * <p> + * In this test all file are present only in the working tree. + * </p> + * + * @throws IOException + * @throws GitAPIException + * @throws NoFilepatternException + */ + @Test + public void testHierarchyWorktreeOnly() + throws IOException, NoFilepatternException, GitAPIException { + writeAttributesFile(".git/info/attributes", "*.global eol=crlf"); + writeAttributesFile(".gitattributes", "*.local eol=lf"); + writeAttributesFile("level1/.gitattributes", "*.local text"); + writeAttributesFile("level1/level2/.gitattributes", "*.local -text"); + + writeTrashFile("l0.global", ""); + writeTrashFile("l0.local", ""); + + writeTrashFile("level1/l1.global", ""); + writeTrashFile("level1/l1.local", ""); + + writeTrashFile("level1/level2/l2.global", ""); + writeTrashFile("level1/level2/l2.local", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + assertEntry(F, "l0.global", asSet(EOL_CRLF)); + assertEntry(F, "l0.local", asSet(EOL_LF)); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + assertEntry(F, "level1/l1.global", asSet(EOL_CRLF)); + assertEntry(F, "level1/l1.local", asSet(EOL_LF, TEXT_SET)); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/.gitattributes"); + assertEntry(F, "level1/level2/l2.global", asSet(EOL_CRLF)); + assertEntry(F, "level1/level2/l2.local", asSet(EOL_LF, TEXT_UNSET)); + + endWalk(); + + } + + /** + * Checks that the list of attributes is an aggregation of all the + * attributes from the attributes files hierarchy. + * + * @throws IOException + */ + @Test + public void testAggregation() throws IOException { + writeGlobalAttributeFile("globalAttributesFile", "*.txt -custom2"); + writeAttributesFile(".git/info/attributes", "*.txt eol=crlf"); + writeAttributesFile(".gitattributes", "*.txt custom=root"); + writeAttributesFile("level1/.gitattributes", "*.txt text"); + writeAttributesFile("level1/level2/.gitattributes", "*.txt -delta"); + + writeTrashFile("l0.txt", ""); + + writeTrashFile("level1/l1.txt", ""); + + writeTrashFile("level1/level2/l2.txt", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + assertEntry(F, "l0.txt", asSet(EOL_CRLF, CUSTOM_ROOT, CUSTOM2_UNSET)); + + assertEntry(D, "level1"); + assertEntry(F, "level1/.gitattributes"); + assertEntry(F, "level1/l1.txt", + asSet(EOL_CRLF, CUSTOM_ROOT, TEXT_SET, CUSTOM2_UNSET)); + + assertEntry(D, "level1/level2"); + assertEntry(F, "level1/level2/.gitattributes"); + assertEntry( + F, + "level1/level2/l2.txt", + asSet(EOL_CRLF, CUSTOM_ROOT, TEXT_SET, DELTA_UNSET, + CUSTOM2_UNSET)); + + endWalk(); + + } + + /** + * Checks that the last entry in .gitattributes is used if 2 lines match the + * same attribute + * + * @throws IOException + */ + @Test + public void testOverriding() throws IOException { + writeAttributesFile(".git/info/attributes",// + // + "*.txt custom=current",// + "*.txt custom=parent",// + "*.txt custom=root",// + "*.txt custom=info", + // + "*.txt delta",// + "*.txt -delta", + // + "*.txt eol=lf",// + "*.txt eol=crlf", + // + "*.txt text",// + "*.txt -text"); + + writeTrashFile("l0.txt", ""); + beginWalk(); + + assertEntry(F, "l0.txt", + asSet(TEXT_UNSET, EOL_CRLF, DELTA_UNSET, CUSTOM_INFO)); + + endWalk(); + } + + /** + * Checks that the last value of an attribute is used if in the same line an + * attribute is defined several time. + * + * @throws IOException + */ + @Test + public void testOverriding2() throws IOException { + writeAttributesFile(".git/info/attributes", + "*.txt custom=current custom=parent custom=root custom=info",// + "*.txt delta -delta",// + "*.txt eol=lf eol=crlf",// + "*.txt text -text"); + writeTrashFile("l0.txt", ""); + beginWalk(); + + assertEntry(F, "l0.txt", + asSet(TEXT_UNSET, EOL_CRLF, DELTA_UNSET, CUSTOM_INFO)); + + endWalk(); + } + + @Test + public void testRulesInherited() throws Exception { + writeAttributesFile(".gitattributes", "**/*.txt eol=lf"); + writeTrashFile("src/config/readme.txt", ""); + writeTrashFile("src/config/windows.file", ""); + + beginWalk(); + + assertEntry(F, ".gitattributes"); + assertEntry(D, "src"); + assertEntry(D, "src/config"); + + assertEntry(F, "src/config/readme.txt", asSet(EOL_LF)); + assertEntry(F, "src/config/windows.file", + Collections.<Attribute> emptySet()); + + endWalk(); + } + + private void beginWalk() throws NoWorkTreeException, IOException { + walk = new TreeWalk(db); + walk.addTree(new FileTreeIterator(db)); + walk.addTree(new DirCacheIterator(db.readDirCache())); + + ci_walk = new TreeWalk(db); + ci_walk.setOperationType(OperationType.CHECKIN_OP); + ci_walk.addTree(new FileTreeIterator(db)); + ci_walk.addTree(new DirCacheIterator(db.readDirCache())); + } + + /** + * Assert an entry in which checkin and checkout attributes are expected to + * be the same. + * + * @param type + * @param pathName + * @param forBothOperaiton + * @throws IOException + */ + private void assertEntry(FileMode type, String pathName, + Set<Attribute> forBothOperaiton) throws IOException { + assertEntry(type, pathName, forBothOperaiton, forBothOperaiton); + } + + /** + * Assert an entry with no attribute expected. + * + * @param type + * @param pathName + * @throws IOException + */ + private void assertEntry(FileMode type, String pathName) throws IOException { + assertEntry(type, pathName, Collections.<Attribute> emptySet(), + Collections.<Attribute> emptySet()); + } + + /** + * Assert that an entry; + * <ul> + * <li>Has the correct type</li> + * <li>Exist in the tree walk</li> + * <li>Has the expected attributes on a checkin operation</li> + * <li>Has the expected attributes on a checkout operation</li> + * </ul> + * + * @param type + * @param pathName + * @param checkinAttributes + * @param checkoutAttributes + * @throws IOException + */ + private void assertEntry(FileMode type, String pathName, + Set<Attribute> checkinAttributes, Set<Attribute> checkoutAttributes) + throws IOException { + assertTrue("walk has entry", walk.next()); + assertTrue("walk has entry", ci_walk.next()); + assertEquals(pathName, walk.getPathString()); + assertEquals(type, walk.getFileMode(0)); + + assertEquals(checkinAttributes, + asSet(ci_walk.getAttributes().getAll())); + assertEquals(checkoutAttributes, + asSet(walk.getAttributes().getAll())); + + if (D.equals(type)) { + walk.enterSubtree(); + ci_walk.enterSubtree(); + } + } + + private static Set<Attribute> asSet(Collection<Attribute> attributes) { + Set<Attribute> ret = new HashSet<>(); + for (Attribute a : attributes) { + ret.add(a); + } + return ret; + } + + private File writeAttributesFile(String name, String... rules) + throws IOException { + StringBuilder data = new StringBuilder(); + for (String line : rules) + data.append(line + "\n"); + return writeTrashFile(name, data.toString()); + } + + /** + * Creates an attributes file and set its locationĀ in the git configuration. + * + * @param fileName + * @param attributes + * @return The attribute file + * @throws IOException + * @see Repository#getConfig() + */ + private File writeGlobalAttributeFile(String fileName, String... attributes) + throws IOException { + customAttributeFile = File.createTempFile("tmp_", fileName, null); + customAttributeFile.deleteOnExit(); + StringBuilder attributesFileContent = new StringBuilder(); + for (String attr : attributes) { + attributesFileContent.append(attr).append("\n"); + } + JGitTestUtil.write(customAttributeFile, + attributesFileContent.toString()); + db.getConfig().setString("core", null, "attributesfile", + customAttributeFile.getAbsolutePath()); + return customAttributeFile; + } + + static Set<Attribute> asSet(Attribute... attrs) { + HashSet<Attribute> result = new HashSet<>(); + result.addAll(Arrays.asList(attrs)); + return result; + } + + private void endWalk() throws IOException { + assertFalse("Not all files tested", walk.next()); + assertFalse("Not all files tested", ci_walk.next()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java new file mode 100644 index 0000000000..ac30c6c526 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.attributes.merge; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.function.Consumer; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.MergeResult.MergeStatus; +import org.eclipse.jgit.api.errors.CheckoutConflictException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidMergeHeadsException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.junit.Test; + +public class MergeGitAttributeTest extends RepositoryTestCase { + + private static final String REFS_HEADS_RIGHT = "refs/heads/right"; + + private static final String REFS_HEADS_MASTER = "refs/heads/master"; + + private static final String REFS_HEADS_LEFT = "refs/heads/left"; + + private static final String DISABLE_CHECK_BRANCH = "refs/heads/disabled_checked"; + + private static final String ENABLE_CHECKED_BRANCH = "refs/heads/enabled_checked"; + + private static final String ENABLED_CHECKED_GIF = "enabled_checked.gif"; + + public Git createRepositoryBinaryConflict(Consumer<Git> initialCommit, + Consumer<Git> leftCommit, Consumer<Git> rightCommit) + throws NoFilepatternException, GitAPIException, NoWorkTreeException, + IOException { + // Set up a git whith conflict commits on images + Git git = new Git(db); + + // First commit + initialCommit.accept(git); + git.add().addFilepattern(".").call(); + RevCommit firstCommit = git.commit().setAll(true) + .setMessage("initial commit adding git attribute file").call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, REFS_HEADS_LEFT); + checkoutBranch(REFS_HEADS_LEFT); + leftCommit.accept(git); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Left").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, REFS_HEADS_RIGHT); + checkoutBranch(REFS_HEADS_RIGHT); + rightCommit.accept(git); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Right").call(); + + checkoutBranch(REFS_HEADS_LEFT); + return git; + + } + + @Test + public void mergeTextualFile_NoAttr() throws NoWorkTreeException, + NoFilepatternException, GitAPIException, IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })) { + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + assertNull(mergeResult.getConflicts()); + + // Check that the image was not modified (not conflict marker added) + String result = read( + writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_UnsetMerge_Conflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat -merge"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })) { + // Check that the merge attribute is unset + assertAddMergeAttributeUnset(REFS_HEADS_LEFT, "main.cat"); + assertAddMergeAttributeUnset(REFS_HEADS_RIGHT, "main.cat"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + String catContent = read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile()); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + assertEquals(catContent, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_UnsetMerge_NoConflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.txt -merge"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })) { + // Check that the merge attribute is unset + assertAddMergeAttributeUndefined(REFS_HEADS_LEFT, "main.cat"); + assertAddMergeAttributeUndefined(REFS_HEADS_RIGHT, "main.cat"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + String result = read( + writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_SetBinaryMerge_Conflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat merge=binary"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })) { + // Check that the merge attribute is set to binary + assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", + "binary"); + assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat", + "binary"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + String catContent = read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile()); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + assertEquals(catContent, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_SetUnionMerge() throws NoWorkTreeException, + NoFilepatternException, GitAPIException, IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat merge=union"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "G\n" + "C\n" + "F\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })) { + // Check that the merge attribute is set to union + assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", "union"); + assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat", + "union"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/left -> refs/heads/right + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + // Check that the file is the union of both branches (no conflict + // marker added) + String result = read(writeTrashFile("res.cat", + "A\n" + "G\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile())); + } + } + + @Test + public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException, + IOException, NoHeadException, ConcurrentRefUpdateException, + CheckoutConflictException, InvalidMergeHeadsException, + WrongRepositoryStateException, NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + // Set up a git with conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), ""); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is unset + assertAddMergeAttributeUndefined(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeUndefined(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (no conflict marker added) + try (FileInputStream mergeResultFile = new FileInputStream( + db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF) + .toFile())) { + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } + } + } + + @Test + public void mergeBinaryFile_UnsetMerge_Conflict() + throws IllegalStateException, + IOException, NoHeadException, ConcurrentRefUpdateException, + CheckoutConflictException, InvalidMergeHeadsException, + WrongRepositoryStateException, NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + // Set up a git whith conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), "*.gif -merge"); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is unset + assertAddMergeAttributeUnset(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeUnset(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + try (FileInputStream mergeResultFile = new FileInputStream( + db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF) + .toFile())) { + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } + } + } + + @Test + public void mergeBinaryFile_SetMerge_Conflict() + throws IllegalStateException, IOException, NoHeadException, + ConcurrentRefUpdateException, CheckoutConflictException, + InvalidMergeHeadsException, WrongRepositoryStateException, + NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + // Set up a git whith conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), "*.gif merge"); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is set + assertAddMergeAttributeSet(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeSet(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + try (FileInputStream mergeResultFile = new FileInputStream( + db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF) + .toFile())) { + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } + } + } + + /* + * Copied from org.apache.commons.io.IOUtils + */ + private boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (input1 == input2) { + return true; + } + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + final int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + final int ch2 = input2.read(); + return ch2 == -1; + } + + private void assertAddMergeAttributeUnset(String branch, String fileName) + throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.UNSET, mergeAttribute.getState()); + } + } + + private void assertAddMergeAttributeSet(String branch, String fileName) + throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.SET, mergeAttribute.getState()); + } + } + + private void assertAddMergeAttributeUndefined(String branch, + String fileName) throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNull(mergeAttribute); + } + } + + private void assertAddMergeAttributeCustom(String branch, String fileName, + String value) throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.CUSTOM, mergeAttribute.getState()); + assertEquals(value, mergeAttribute.getValue()); + } + } + + private void copy(String resourcePath, String resourceNewName, + String pathInRepo) throws IOException { + InputStream input = getClass().getResourceAsStream(resourcePath); + Files.copy(input, db.getWorkTree().toPath().resolve(pathInRepo) + .resolve(resourceNewName)); + } + +} |