/* * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com> * * 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 java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; 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 TreeWalk} * <p> * 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 <b>binary</b> rule that is present in any git folder * <code>[attr]binary -diff -merge -text</code> */ private static final List<Attribute> BINARY_RULE_ATTRIBUTES = new AttributesRule( MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$ .getAttributes(); private final TreeWalk treeWalk; private final AttributesNode globalNode; private final AttributesNode infoNode; private final Map<String, List<Attribute>> expansions = new HashMap<>(); /** * Create an {@link AttributesHandler} with default rules as well as merged * rules from global, info and worktree root attributes * * @param treeWalk * @throws IOException */ public AttributesHandler(TreeWalk treeWalk) throws IOException { this.treeWalk = treeWalk; 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(treeWalk .getTree(CanonicalTreeParser.class))); 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 TreeWalk#getAttributes()} * * @return the {@link Attributes} for the current path represented by the * {@link TreeWalk} * @throws IOException */ 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, isDirectory, treeWalk.getTree(WorkingTreeIterator.class), treeWalk.getTree(DirCacheIterator.class), treeWalk.getTree(CanonicalTreeParser.class), 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 isDirectory * true if the target item is a directory. * @param workingTreeIterator * @param dirCacheIterator * @param otherTree * @param result * that will hold the attributes matching this entry path. This * method will NOT override any existing entry in attributes. * @throws IOException */ private void mergePerDirectoryEntryAttributes(String entryPath, 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, isDirectory, result); } mergePerDirectoryEntryAttributes(entryPath, 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<AttributesRule> rules = node.getRules(); // Parse rules in the reverse order that they were read since the last // entry should be used ListIterator<AttributesRule> ruleIterator = rules .listIterator(rules.size()); while (ruleIterator.hasPrevious()) { AttributesRule rule = ruleIterator.previous(); if (rule.isMatch(entryPath, isDirectory)) { ListIterator<Attribute> 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); } } } } /** * @param attr * @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<Attribute> 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. * <p> * This method implements the fallback mechanism between the index and the * working tree depending on the operation type * </p> * * @param treeWalk * @param workingTreeIterator * @param dirCacheIterator * @param otherTree * @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 extends AbstractTreeIterator> T parentOf(@Nullable T node) { if(node==null) return null; @SuppressWarnings("unchecked") Class<T> type = (Class<T>) node.getClass(); AbstractTreeIterator parent = node.parent; if (type.isInstance(parent)) { return type.cast(parent); } return null; } private static <T extends AbstractTreeIterator> 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<T> type = (Class<T>) node.getClass(); if (type.isInstance(t)) { return type.cast(t); } return null; } }