AttributesHandler.java

  1. /*
  2.  * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
  3.  *
  4.  * This program and the accompanying materials are made available
  5.  * under the terms of the Eclipse Distribution License v1.0 which
  6.  * accompanies this distribution, is reproduced below, and is
  7.  * available at http://www.eclipse.org/org/documents/edl-v10.php
  8.  *
  9.  * All rights reserved.
  10.  *
  11.  * Redistribution and use in source and binary forms, with or
  12.  * without modification, are permitted provided that the following
  13.  * conditions are met:
  14.  *
  15.  * - Redistributions of source code must retain the above copyright
  16.  *   notice, this list of conditions and the following disclaimer.
  17.  *
  18.  * - Redistributions in binary form must reproduce the above
  19.  *   copyright notice, this list of conditions and the following
  20.  *   disclaimer in the documentation and/or other materials provided
  21.  *   with the distribution.
  22.  *
  23.  * - Neither the name of the Eclipse Foundation, Inc. nor the
  24.  *   names of its contributors may be used to endorse or promote
  25.  *   products derived from this software without specific prior
  26.  *   written permission.
  27.  *
  28.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  29.  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  30.  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  31.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  32.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  33.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  34.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  35.  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  36.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  37.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  38.  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  39.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  40.  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  41.  */
  42. package org.eclipse.jgit.attributes;

  43. import java.io.IOException;
  44. import java.util.HashMap;
  45. import java.util.List;
  46. import java.util.ListIterator;
  47. import java.util.Map;

  48. import org.eclipse.jgit.annotations.Nullable;
  49. import org.eclipse.jgit.attributes.Attribute.State;
  50. import org.eclipse.jgit.dircache.DirCacheIterator;
  51. import org.eclipse.jgit.lib.FileMode;
  52. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  53. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  54. import org.eclipse.jgit.treewalk.TreeWalk;
  55. import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
  56. import org.eclipse.jgit.treewalk.WorkingTreeIterator;

  57. /**
  58.  * The attributes handler knows how to retrieve, parse and merge attributes from
  59.  * the various gitattributes files. Furthermore it collects and expands macro
  60.  * expressions. The method {@link #getAttributes()} yields the ready processed
  61.  * attributes for the current path represented by the
  62.  * {@link org.eclipse.jgit.treewalk.TreeWalk}
  63.  * <p>
  64.  * The implementation is based on the specifications in
  65.  * http://git-scm.com/docs/gitattributes
  66.  *
  67.  * @since 4.3
  68.  */
  69. public class AttributesHandler {
  70.     private static final String MACRO_PREFIX = "[attr]"; //$NON-NLS-1$

  71.     private static final String BINARY_RULE_KEY = "binary"; //$NON-NLS-1$

  72.     /**
  73.      * This is the default <b>binary</b> rule that is present in any git folder
  74.      * <code>[attr]binary -diff -merge -text</code>
  75.      */
  76.     private static final List<Attribute> BINARY_RULE_ATTRIBUTES = new AttributesRule(
  77.             MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$
  78.                     .getAttributes();

  79.     private final TreeWalk treeWalk;

  80.     private final AttributesNode globalNode;

  81.     private final AttributesNode infoNode;

  82.     private final Map<String, List<Attribute>> expansions = new HashMap<>();

  83.     /**
  84.      * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with
  85.      * default rules as well as merged rules from global, info and worktree root
  86.      * attributes
  87.      *
  88.      * @param treeWalk
  89.      *            a {@link org.eclipse.jgit.treewalk.TreeWalk}
  90.      * @throws java.io.IOException
  91.      */
  92.     public AttributesHandler(TreeWalk treeWalk) throws IOException {
  93.         this.treeWalk = treeWalk;
  94.         AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider();
  95.         this.globalNode = attributesNodeProvider != null
  96.                 ? attributesNodeProvider.getGlobalAttributesNode() : null;
  97.         this.infoNode = attributesNodeProvider != null
  98.                 ? attributesNodeProvider.getInfoAttributesNode() : null;

  99.         AttributesNode rootNode = attributesNode(treeWalk,
  100.                 rootOf(
  101.                         treeWalk.getTree(WorkingTreeIterator.class)),
  102.                 rootOf(
  103.                         treeWalk.getTree(DirCacheIterator.class)),
  104.                 rootOf(treeWalk
  105.                         .getTree(CanonicalTreeParser.class)));

  106.         expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES);
  107.         for (AttributesNode node : new AttributesNode[] { globalNode, rootNode,
  108.                 infoNode }) {
  109.             if (node == null) {
  110.                 continue;
  111.             }
  112.             for (AttributesRule rule : node.getRules()) {
  113.                 if (rule.getPattern().startsWith(MACRO_PREFIX)) {
  114.                     expansions.put(rule.getPattern()
  115.                             .substring(MACRO_PREFIX.length()).trim(),
  116.                             rule.getAttributes());
  117.                 }
  118.             }
  119.         }
  120.     }

  121.     /**
  122.      * See {@link org.eclipse.jgit.treewalk.TreeWalk#getAttributes()}
  123.      *
  124.      * @return the {@link org.eclipse.jgit.attributes.Attributes} for the
  125.      *         current path represented by the
  126.      *         {@link org.eclipse.jgit.treewalk.TreeWalk}
  127.      * @throws java.io.IOException
  128.      */
  129.     public Attributes getAttributes() throws IOException {
  130.         String entryPath = treeWalk.getPathString();
  131.         boolean isDirectory = (treeWalk.getFileMode() == FileMode.TREE);
  132.         Attributes attributes = new Attributes();

  133.         // Gets the info attributes
  134.         mergeInfoAttributes(entryPath, isDirectory, attributes);

  135.         // Gets the attributes located on the current entry path
  136.         mergePerDirectoryEntryAttributes(entryPath, entryPath.lastIndexOf('/'),
  137.                 isDirectory,
  138.                 treeWalk.getTree(WorkingTreeIterator.class),
  139.                 treeWalk.getTree(DirCacheIterator.class),
  140.                 treeWalk.getTree(CanonicalTreeParser.class),
  141.                 attributes);

  142.         // Gets the attributes located in the global attribute file
  143.         mergeGlobalAttributes(entryPath, isDirectory, attributes);

  144.         // now after all attributes are collected - in the correct hierarchy
  145.         // order - remove all unspecified entries (the ! marker)
  146.         for (Attribute a : attributes.getAll()) {
  147.             if (a.getState() == State.UNSPECIFIED)
  148.                 attributes.remove(a.getKey());
  149.         }

  150.         return attributes;
  151.     }

  152.     /**
  153.      * Merges the matching GLOBAL attributes for an entry path.
  154.      *
  155.      * @param entryPath
  156.      *            the path to test. The path must be relative to this attribute
  157.      *            node's own repository path, and in repository path format
  158.      *            (uses '/' and not '\').
  159.      * @param isDirectory
  160.      *            true if the target item is a directory.
  161.      * @param result
  162.      *            that will hold the attributes matching this entry path. This
  163.      *            method will NOT override any existing entry in attributes.
  164.      */
  165.     private void mergeGlobalAttributes(String entryPath, boolean isDirectory,
  166.             Attributes result) {
  167.         mergeAttributes(globalNode, entryPath, isDirectory, result);
  168.     }

  169.     /**
  170.      * Merges the matching INFO attributes for an entry path.
  171.      *
  172.      * @param entryPath
  173.      *            the path to test. The path must be relative to this attribute
  174.      *            node's own repository path, and in repository path format
  175.      *            (uses '/' and not '\').
  176.      * @param isDirectory
  177.      *            true if the target item is a directory.
  178.      * @param result
  179.      *            that will hold the attributes matching this entry path. This
  180.      *            method will NOT override any existing entry in attributes.
  181.      */
  182.     private void mergeInfoAttributes(String entryPath, boolean isDirectory,
  183.             Attributes result) {
  184.         mergeAttributes(infoNode, entryPath, isDirectory, result);
  185.     }

  186.     /**
  187.      * Merges the matching working directory attributes for an entry path.
  188.      *
  189.      * @param entryPath
  190.      *            the path to test. The path must be relative to this attribute
  191.      *            node's own repository path, and in repository path format
  192.      *            (uses '/' and not '\').
  193.      * @param nameRoot
  194.      *            index of the '/' preceeding the current level, or -1 if none
  195.      * @param isDirectory
  196.      *            true if the target item is a directory.
  197.      * @param workingTreeIterator
  198.      * @param dirCacheIterator
  199.      * @param otherTree
  200.      * @param result
  201.      *            that will hold the attributes matching this entry path. This
  202.      *            method will NOT override any existing entry in attributes.
  203.      * @throws IOException
  204.      */
  205.     private void mergePerDirectoryEntryAttributes(String entryPath,
  206.             int nameRoot, boolean isDirectory,
  207.             @Nullable WorkingTreeIterator workingTreeIterator,
  208.             @Nullable DirCacheIterator dirCacheIterator,
  209.             @Nullable CanonicalTreeParser otherTree, Attributes result)
  210.                     throws IOException {
  211.         // Prevents infinite recurrence
  212.         if (workingTreeIterator != null || dirCacheIterator != null
  213.                 || otherTree != null) {
  214.             AttributesNode attributesNode = attributesNode(
  215.                     treeWalk, workingTreeIterator, dirCacheIterator, otherTree);
  216.             if (attributesNode != null) {
  217.                 mergeAttributes(attributesNode,
  218.                         entryPath.substring(nameRoot + 1), isDirectory,
  219.                         result);
  220.             }
  221.             mergePerDirectoryEntryAttributes(entryPath,
  222.                     entryPath.lastIndexOf('/', nameRoot - 1), isDirectory,
  223.                     parentOf(workingTreeIterator), parentOf(dirCacheIterator),
  224.                     parentOf(otherTree), result);
  225.         }
  226.     }

  227.     /**
  228.      * Merges the matching node attributes for an entry path.
  229.      *
  230.      * @param node
  231.      *            the node to scan for matches to entryPath
  232.      * @param entryPath
  233.      *            the path to test. The path must be relative to this attribute
  234.      *            node's own repository path, and in repository path format
  235.      *            (uses '/' and not '\').
  236.      * @param isDirectory
  237.      *            true if the target item is a directory.
  238.      * @param result
  239.      *            that will hold the attributes matching this entry path. This
  240.      *            method will NOT override any existing entry in attributes.
  241.      */
  242.     protected void mergeAttributes(@Nullable AttributesNode node,
  243.             String entryPath,
  244.             boolean isDirectory, Attributes result) {
  245.         if (node == null)
  246.             return;
  247.         List<AttributesRule> rules = node.getRules();
  248.         // Parse rules in the reverse order that they were read since the last
  249.         // entry should be used
  250.         ListIterator<AttributesRule> ruleIterator = rules
  251.                 .listIterator(rules.size());
  252.         while (ruleIterator.hasPrevious()) {
  253.             AttributesRule rule = ruleIterator.previous();
  254.             if (rule.isMatch(entryPath, isDirectory)) {
  255.                 ListIterator<Attribute> attributeIte = rule.getAttributes()
  256.                         .listIterator(rule.getAttributes().size());
  257.                 // Parses the attributes in the reverse order that they were
  258.                 // read since the last entry should be used
  259.                 while (attributeIte.hasPrevious()) {
  260.                     expandMacro(attributeIte.previous(), result);
  261.                 }
  262.             }
  263.         }
  264.     }

  265.     /**
  266.      * Expand a macro
  267.      *
  268.      * @param attr
  269.      *            a {@link org.eclipse.jgit.attributes.Attribute}
  270.      * @param result
  271.      *            contains the (recursive) expanded and merged macro attributes
  272.      *            including the attribute iself
  273.      */
  274.     protected void expandMacro(Attribute attr, Attributes result) {
  275.         // loop detection = exists check
  276.         if (result.containsKey(attr.getKey()))
  277.             return;

  278.         // also add macro to result set, same does native git
  279.         result.put(attr);

  280.         List<Attribute> expansion = expansions.get(attr.getKey());
  281.         if (expansion == null) {
  282.             return;
  283.         }
  284.         switch (attr.getState()) {
  285.         case UNSET: {
  286.             for (Attribute e : expansion) {
  287.                 switch (e.getState()) {
  288.                 case SET:
  289.                     expandMacro(new Attribute(e.getKey(), State.UNSET), result);
  290.                     break;
  291.                 case UNSET:
  292.                     expandMacro(new Attribute(e.getKey(), State.SET), result);
  293.                     break;
  294.                 case UNSPECIFIED:
  295.                     expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
  296.                             result);
  297.                     break;
  298.                 case CUSTOM:
  299.                 default:
  300.                     expandMacro(e, result);
  301.                 }
  302.             }
  303.             break;
  304.         }
  305.         case CUSTOM: {
  306.             for (Attribute e : expansion) {
  307.                 switch (e.getState()) {
  308.                 case SET:
  309.                 case UNSET:
  310.                 case UNSPECIFIED:
  311.                     expandMacro(e, result);
  312.                     break;
  313.                 case CUSTOM:
  314.                 default:
  315.                     expandMacro(new Attribute(e.getKey(), attr.getValue()),
  316.                             result);
  317.                 }
  318.             }
  319.             break;
  320.         }
  321.         case UNSPECIFIED: {
  322.             for (Attribute e : expansion) {
  323.                 expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
  324.                         result);
  325.             }
  326.             break;
  327.         }
  328.         case SET:
  329.         default:
  330.             for (Attribute e : expansion) {
  331.                 expandMacro(e, result);
  332.             }
  333.             break;
  334.         }
  335.     }

  336.     /**
  337.      * Get the {@link AttributesNode} for the current entry.
  338.      * <p>
  339.      * This method implements the fallback mechanism between the index and the
  340.      * working tree depending on the operation type
  341.      * </p>
  342.      *
  343.      * @param treeWalk
  344.      * @param workingTreeIterator
  345.      * @param dirCacheIterator
  346.      * @param otherTree
  347.      * @return a {@link AttributesNode} of the current entry,
  348.      *         {@link NullPointerException} otherwise.
  349.      * @throws IOException
  350.      *             It raises an {@link IOException} if a problem appears while
  351.      *             parsing one on the attributes file.
  352.      */
  353.     private static AttributesNode attributesNode(TreeWalk treeWalk,
  354.             @Nullable WorkingTreeIterator workingTreeIterator,
  355.             @Nullable DirCacheIterator dirCacheIterator,
  356.             @Nullable CanonicalTreeParser otherTree) throws IOException {
  357.         AttributesNode attributesNode = null;
  358.         switch (treeWalk.getOperationType()) {
  359.         case CHECKIN_OP:
  360.             if (workingTreeIterator != null) {
  361.                 attributesNode = workingTreeIterator.getEntryAttributesNode();
  362.             }
  363.             if (attributesNode == null && dirCacheIterator != null) {
  364.                 attributesNode = dirCacheIterator
  365.                         .getEntryAttributesNode(treeWalk.getObjectReader());
  366.             }
  367.             if (attributesNode == null && otherTree != null) {
  368.                 attributesNode = otherTree
  369.                         .getEntryAttributesNode(treeWalk.getObjectReader());
  370.             }
  371.             break;
  372.         case CHECKOUT_OP:
  373.             if (otherTree != null) {
  374.                 attributesNode = otherTree
  375.                         .getEntryAttributesNode(treeWalk.getObjectReader());
  376.             }
  377.             if (attributesNode == null && dirCacheIterator != null) {
  378.                 attributesNode = dirCacheIterator
  379.                         .getEntryAttributesNode(treeWalk.getObjectReader());
  380.             }
  381.             if (attributesNode == null && workingTreeIterator != null) {
  382.                 attributesNode = workingTreeIterator.getEntryAttributesNode();
  383.             }
  384.             break;
  385.         default:
  386.             throw new IllegalStateException(
  387.                     "The only supported operation types are:" //$NON-NLS-1$
  388.                             + OperationType.CHECKIN_OP + "," //$NON-NLS-1$
  389.                             + OperationType.CHECKOUT_OP);
  390.         }

  391.         return attributesNode;
  392.     }

  393.     private static <T extends AbstractTreeIterator> T parentOf(@Nullable T node) {
  394.         if(node==null) return null;
  395.         @SuppressWarnings("unchecked")
  396.         Class<T> type = (Class<T>) node.getClass();
  397.         AbstractTreeIterator parent = node.parent;
  398.         if (type.isInstance(parent)) {
  399.             return type.cast(parent);
  400.         }
  401.         return null;
  402.     }

  403.     private static <T extends AbstractTreeIterator> T rootOf(
  404.             @Nullable T node) {
  405.         if(node==null) return null;
  406.         AbstractTreeIterator t=node;
  407.         while (t!= null && t.parent != null) {
  408.             t= t.parent;
  409.         }
  410.         @SuppressWarnings("unchecked")
  411.         Class<T> type = (Class<T>) node.getClass();
  412.         if (type.isInstance(t)) {
  413.             return type.cast(t);
  414.         }
  415.         return null;
  416.     }

  417. }