| /* |
| * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| package org.eclipse.jgit.attributes; |
| |
| import static org.junit.Assert.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.Constants; |
| 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(Constants.CHARSET))); |
| 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()), |
| Constants.CHARSET))) { |
| r.lines().forEach(line -> { |
| // Parse the line and add to result map |
| int start = 0; |
| int i = line.indexOf(':'); |
| String path = line.substring(0, i).trim(); |
| 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(); |
| } |
| } |