| /* |
| * 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 org.eclipse.jgit.treewalk.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 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 |
| */ |
| 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 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 |
| */ |
| 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), |
| 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 nameRoot |
| * index of the '/' preceeding the current level, or -1 if none |
| * @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, |
| 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<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); |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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<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; |
| } |
| |
| } |