/*
 * 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());
	}
}