/* * Copyright (C) 2015, 2022 Ivan Motsch 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 java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.function.Supplier; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute.State; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeIterator; /** * The attributes handler knows how to retrieve, parse and merge attributes from * the various gitattributes files. Furthermore it collects and expands macro * expressions. The method {@link #getAttributes()} yields the ready processed * attributes for the current path represented by the * {@link org.eclipse.jgit.treewalk.TreeWalk} *

* The implementation is based on the specifications in * http://git-scm.com/docs/gitattributes * * @since 4.3 */ public class AttributesHandler { private static final String MACRO_PREFIX = "[attr]"; //$NON-NLS-1$ private static final String BINARY_RULE_KEY = "binary"; //$NON-NLS-1$ /** * This is the default binary rule that is present in any git folder * [attr]binary -diff -merge -text */ private static final List BINARY_RULE_ATTRIBUTES = new AttributesRule( MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$ .getAttributes(); private final TreeWalk treeWalk; private final Supplier attributesTree; private final AttributesNode globalNode; private final AttributesNode infoNode; private final Map> expansions = new HashMap<>(); /** * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with * default rules as well as merged rules from global, info and worktree root * attributes * * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException * if an IO error occurred * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)} * instead */ @Deprecated public AttributesHandler(TreeWalk treeWalk) throws IOException { this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class)); } /** * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with * default rules as well as merged rules from global, info and worktree root * attributes * * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @param attributesTree * the tree to read .gitattributes from * @throws java.io.IOException * if an IO error occurred * @since 6.1 */ public AttributesHandler(TreeWalk treeWalk, Supplier attributesTree) throws IOException { this.treeWalk = treeWalk; this.attributesTree = attributesTree; AttributesNodeProvider attributesNodeProvider = treeWalk .getAttributesNodeProvider(); this.globalNode = attributesNodeProvider != null ? attributesNodeProvider.getGlobalAttributesNode() : null; this.infoNode = attributesNodeProvider != null ? attributesNodeProvider.getInfoAttributesNode() : null; AttributesNode rootNode = attributesNode(treeWalk, rootOf(treeWalk.getTree(WorkingTreeIterator.class)), rootOf(treeWalk.getTree(DirCacheIterator.class)), rootOf(attributesTree.get())); expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES); for (AttributesNode node : new AttributesNode[] { globalNode, rootNode, infoNode }) { if (node == null) { continue; } for (AttributesRule rule : node.getRules()) { if (rule.getPattern().startsWith(MACRO_PREFIX)) { expansions.put(rule.getPattern() .substring(MACRO_PREFIX.length()).trim(), rule.getAttributes()); } } } } /** * See {@link org.eclipse.jgit.treewalk.TreeWalk#getAttributes()} * * @return the {@link org.eclipse.jgit.attributes.Attributes} for the * current path represented by the * {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException * if an IO error occurred */ public Attributes getAttributes() throws IOException { String entryPath = treeWalk.getPathString(); boolean isDirectory = (treeWalk.getFileMode() == FileMode.TREE); Attributes attributes = new Attributes(); // Gets the info attributes mergeInfoAttributes(entryPath, isDirectory, attributes); // Gets the attributes located on the current entry path mergePerDirectoryEntryAttributes(entryPath, entryPath.lastIndexOf('/'), isDirectory, treeWalk.getTree(WorkingTreeIterator.class), treeWalk.getTree(DirCacheIterator.class), attributesTree.get(), attributes); // Gets the attributes located in the global attribute file mergeGlobalAttributes(entryPath, isDirectory, attributes); // now after all attributes are collected - in the correct hierarchy // order - remove all unspecified entries (the ! marker) for (Attribute a : attributes.getAll()) { if (a.getState() == State.UNSPECIFIED) attributes.remove(a.getKey()); } return attributes; } /** * Merges the matching GLOBAL attributes for an entry path. * * @param entryPath * the path to test. The path must be relative to this attribute * node's own repository path, and in repository path format * (uses '/' and not '\'). * @param isDirectory * true if the target item is a directory. * @param result * that will hold the attributes matching this entry path. This * method will NOT override any existing entry in attributes. */ private void mergeGlobalAttributes(String entryPath, boolean isDirectory, Attributes result) { mergeAttributes(globalNode, entryPath, isDirectory, result); } /** * Merges the matching INFO attributes for an entry path. * * @param entryPath * the path to test. The path must be relative to this attribute * node's own repository path, and in repository path format * (uses '/' and not '\'). * @param isDirectory * true if the target item is a directory. * @param result * that will hold the attributes matching this entry path. This * method will NOT override any existing entry in attributes. */ private void mergeInfoAttributes(String entryPath, boolean isDirectory, Attributes result) { mergeAttributes(infoNode, entryPath, isDirectory, result); } /** * Merges the matching working directory attributes for an entry path. * * @param entryPath * the path to test. The path must be relative to this attribute * node's own repository path, and in repository path format * (uses '/' and not '\'). * @param nameRoot * index of the '/' preceeding the current level, or -1 if none * @param isDirectory * true if the target item is a directory. * @param workingTreeIterator * the working tree iterator * @param dirCacheIterator * the dircache iterator * @param otherTree * another tree * @param result * that will hold the attributes matching this entry path. This * method will NOT override any existing entry in attributes. * @throws IOException * if an IO error occurred */ private void mergePerDirectoryEntryAttributes(String entryPath, int nameRoot, boolean isDirectory, @Nullable WorkingTreeIterator workingTreeIterator, @Nullable DirCacheIterator dirCacheIterator, @Nullable CanonicalTreeParser otherTree, Attributes result) throws IOException { // Prevents infinite recurrence if (workingTreeIterator != null || dirCacheIterator != null || otherTree != null) { AttributesNode attributesNode = attributesNode( treeWalk, workingTreeIterator, dirCacheIterator, otherTree); if (attributesNode != null) { mergeAttributes(attributesNode, entryPath.substring(nameRoot + 1), isDirectory, result); } mergePerDirectoryEntryAttributes(entryPath, entryPath.lastIndexOf('/', nameRoot - 1), isDirectory, parentOf(workingTreeIterator), parentOf(dirCacheIterator), parentOf(otherTree), result); } } /** * Merges the matching node attributes for an entry path. * * @param node * the node to scan for matches to entryPath * @param entryPath * the path to test. The path must be relative to this attribute * node's own repository path, and in repository path format * (uses '/' and not '\'). * @param isDirectory * true if the target item is a directory. * @param result * that will hold the attributes matching this entry path. This * method will NOT override any existing entry in attributes. */ protected void mergeAttributes(@Nullable AttributesNode node, String entryPath, boolean isDirectory, Attributes result) { if (node == null) return; List rules = node.getRules(); // Parse rules in the reverse order that they were read since the last // entry should be used ListIterator ruleIterator = rules .listIterator(rules.size()); while (ruleIterator.hasPrevious()) { AttributesRule rule = ruleIterator.previous(); if (rule.isMatch(entryPath, isDirectory)) { ListIterator attributeIte = rule.getAttributes() .listIterator(rule.getAttributes().size()); // Parses the attributes in the reverse order that they were // read since the last entry should be used while (attributeIte.hasPrevious()) { expandMacro(attributeIte.previous(), result); } } } } /** * Expand a macro * * @param attr * a {@link org.eclipse.jgit.attributes.Attribute} * @param result * contains the (recursive) expanded and merged macro attributes * including the attribute iself */ protected void expandMacro(Attribute attr, Attributes result) { // loop detection = exists check if (result.containsKey(attr.getKey())) return; // also add macro to result set, same does native git result.put(attr); List expansion = expansions.get(attr.getKey()); if (expansion == null) { return; } switch (attr.getState()) { case UNSET: { for (Attribute e : expansion) { switch (e.getState()) { case SET: expandMacro(new Attribute(e.getKey(), State.UNSET), result); break; case UNSET: expandMacro(new Attribute(e.getKey(), State.SET), result); break; case UNSPECIFIED: expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED), result); break; case CUSTOM: default: expandMacro(e, result); } } break; } case CUSTOM: { for (Attribute e : expansion) { switch (e.getState()) { case SET: case UNSET: case UNSPECIFIED: expandMacro(e, result); break; case CUSTOM: default: expandMacro(new Attribute(e.getKey(), attr.getValue()), result); } } break; } case UNSPECIFIED: { for (Attribute e : expansion) { expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED), result); } break; } case SET: default: for (Attribute e : expansion) { expandMacro(e, result); } break; } } /** * Get the {@link AttributesNode} for the current entry. *

* This method implements the fallback mechanism between the index and the * working tree depending on the operation type *

* * @param treeWalk * used to walk trees * @param workingTreeIterator * used to walk the working tree * @param dirCacheIterator * used to walk the dircache * @param otherTree * another tree * @return a {@link AttributesNode} of the current entry, * {@link NullPointerException} otherwise. * @throws IOException * It raises an {@link IOException} if a problem appears while * parsing one on the attributes file. */ private static AttributesNode attributesNode(TreeWalk treeWalk, @Nullable WorkingTreeIterator workingTreeIterator, @Nullable DirCacheIterator dirCacheIterator, @Nullable CanonicalTreeParser otherTree) throws IOException { AttributesNode attributesNode = null; switch (treeWalk.getOperationType()) { case CHECKIN_OP: if (workingTreeIterator != null) { attributesNode = workingTreeIterator.getEntryAttributesNode(); } if (attributesNode == null && dirCacheIterator != null) { attributesNode = dirCacheIterator .getEntryAttributesNode(treeWalk.getObjectReader()); } if (attributesNode == null && otherTree != null) { attributesNode = otherTree .getEntryAttributesNode(treeWalk.getObjectReader()); } break; case CHECKOUT_OP: if (otherTree != null) { attributesNode = otherTree .getEntryAttributesNode(treeWalk.getObjectReader()); } if (attributesNode == null && dirCacheIterator != null) { attributesNode = dirCacheIterator .getEntryAttributesNode(treeWalk.getObjectReader()); } if (attributesNode == null && workingTreeIterator != null) { attributesNode = workingTreeIterator.getEntryAttributesNode(); } break; default: throw new IllegalStateException( "The only supported operation types are:" //$NON-NLS-1$ + OperationType.CHECKIN_OP + "," //$NON-NLS-1$ + OperationType.CHECKOUT_OP); } return attributesNode; } private static T parentOf(@Nullable T node) { if(node==null) return null; @SuppressWarnings("unchecked") Class type = (Class) node.getClass(); AbstractTreeIterator parent = node.parent; if (type.isInstance(parent)) { return type.cast(parent); } return null; } private static T rootOf( @Nullable T node) { if(node==null) return null; AbstractTreeIterator t=node; while (t!= null && t.parent != null) { t= t.parent; } @SuppressWarnings("unchecked") Class type = (Class) node.getClass(); if (type.isInstance(t)) { return type.cast(t); } return null; } }