/* * 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 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. *

* 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. 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 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. emptySet(), Collections. emptySet()); } /** * Assert that an entry; *
    *
  • Has the correct type
  • *
  • Exist in the tree walk
  • *
  • Has the expected attributes on a checkin operation
  • *
  • Has the expected attributes on a checkout operation
  • *
* * @param type * @param pathName * @param checkinAttributes * @param checkoutAttributes * @throws IOException */ private void assertEntry(FileMode type, String pathName, Set checkinAttributes, Set 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 asSet(Collection attributes) { Set 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 asSet(Attribute... attrs) { HashSet 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()); } }