blob: b6615075c0139e9d1a91656d3ebeb23a67ec275d [file] [log] [blame]
/*
* 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 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 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();
}
}