/* * 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. *
* In this test we changed the content of the attribute files in the working * tree compared to the one in the index. *
* * @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* In this use case files are present in both the working tree and the index *
* * @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. ** In this test all file are present in both the working tree and the index. *
* * @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. ** In this test all file are present only in the working tree. *
* * @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.