AttributesHandler.java

  1. /*
  2.  * Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.attributes;

  11. import java.io.IOException;
  12. import java.util.HashMap;
  13. import java.util.List;
  14. import java.util.ListIterator;
  15. import java.util.Map;
  16. import java.util.function.Supplier;

  17. import org.eclipse.jgit.annotations.Nullable;
  18. import org.eclipse.jgit.attributes.Attribute.State;
  19. import org.eclipse.jgit.dircache.DirCacheIterator;
  20. import org.eclipse.jgit.lib.FileMode;
  21. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  22. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  23. import org.eclipse.jgit.treewalk.TreeWalk;
  24. import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
  25. import org.eclipse.jgit.treewalk.WorkingTreeIterator;

  26. /**
  27.  * The attributes handler knows how to retrieve, parse and merge attributes from
  28.  * the various gitattributes files. Furthermore it collects and expands macro
  29.  * expressions. The method {@link #getAttributes()} yields the ready processed
  30.  * attributes for the current path represented by the
  31.  * {@link org.eclipse.jgit.treewalk.TreeWalk}
  32.  * <p>
  33.  * The implementation is based on the specifications in
  34.  * http://git-scm.com/docs/gitattributes
  35.  *
  36.  * @since 4.3
  37.  */
  38. public class AttributesHandler {
  39.     private static final String MACRO_PREFIX = "[attr]"; //$NON-NLS-1$

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

  41.     /**
  42.      * This is the default <b>binary</b> rule that is present in any git folder
  43.      * <code>[attr]binary -diff -merge -text</code>
  44.      */
  45.     private static final List<Attribute> BINARY_RULE_ATTRIBUTES = new AttributesRule(
  46.             MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$
  47.                     .getAttributes();

  48.     private final TreeWalk treeWalk;

  49.     private final Supplier<CanonicalTreeParser> attributesTree;

  50.     private final AttributesNode globalNode;

  51.     private final AttributesNode infoNode;

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

  53.     /**
  54.      * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with
  55.      * default rules as well as merged rules from global, info and worktree root
  56.      * attributes
  57.      *
  58.      * @param treeWalk
  59.      *            a {@link org.eclipse.jgit.treewalk.TreeWalk}
  60.      * @throws java.io.IOException
  61.      * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)}
  62.      *             instead
  63.      */
  64.     @Deprecated
  65.     public AttributesHandler(TreeWalk treeWalk) throws IOException {
  66.         this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class));
  67.     }

  68.     /**
  69.      * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with
  70.      * default rules as well as merged rules from global, info and worktree root
  71.      * attributes
  72.      *
  73.      * @param treeWalk
  74.      *            a {@link org.eclipse.jgit.treewalk.TreeWalk}
  75.      * @param attributesTree
  76.      *            the tree to read .gitattributes from
  77.      * @throws java.io.IOException
  78.      * @since 6.1
  79.      */
  80.     public AttributesHandler(TreeWalk treeWalk,
  81.             Supplier<CanonicalTreeParser> attributesTree) throws IOException {
  82.         this.treeWalk = treeWalk;
  83.         this.attributesTree = attributesTree;
  84.         AttributesNodeProvider attributesNodeProvider = treeWalk
  85.                 .getAttributesNodeProvider();
  86.         this.globalNode = attributesNodeProvider != null
  87.                 ? attributesNodeProvider.getGlobalAttributesNode() : null;
  88.         this.infoNode = attributesNodeProvider != null
  89.                 ? attributesNodeProvider.getInfoAttributesNode() : null;

  90.         AttributesNode rootNode = attributesNode(treeWalk,
  91.                 rootOf(treeWalk.getTree(WorkingTreeIterator.class)),
  92.                 rootOf(treeWalk.getTree(DirCacheIterator.class)),
  93.                 rootOf(attributesTree.get()));

  94.         expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES);
  95.         for (AttributesNode node : new AttributesNode[] { globalNode, rootNode,
  96.                 infoNode }) {
  97.             if (node == null) {
  98.                 continue;
  99.             }
  100.             for (AttributesRule rule : node.getRules()) {
  101.                 if (rule.getPattern().startsWith(MACRO_PREFIX)) {
  102.                     expansions.put(rule.getPattern()
  103.                             .substring(MACRO_PREFIX.length()).trim(),
  104.                             rule.getAttributes());
  105.                 }
  106.             }
  107.         }
  108.     }

  109.     /**
  110.      * See {@link org.eclipse.jgit.treewalk.TreeWalk#getAttributes()}
  111.      *
  112.      * @return the {@link org.eclipse.jgit.attributes.Attributes} for the
  113.      *         current path represented by the
  114.      *         {@link org.eclipse.jgit.treewalk.TreeWalk}
  115.      * @throws java.io.IOException
  116.      */
  117.     public Attributes getAttributes() throws IOException {
  118.         String entryPath = treeWalk.getPathString();
  119.         boolean isDirectory = (treeWalk.getFileMode() == FileMode.TREE);
  120.         Attributes attributes = new Attributes();

  121.         // Gets the info attributes
  122.         mergeInfoAttributes(entryPath, isDirectory, attributes);

  123.         // Gets the attributes located on the current entry path
  124.         mergePerDirectoryEntryAttributes(entryPath, entryPath.lastIndexOf('/'),
  125.                 isDirectory,
  126.                 treeWalk.getTree(WorkingTreeIterator.class),
  127.                 treeWalk.getTree(DirCacheIterator.class),
  128.                 attributesTree.get(),
  129.                 attributes);

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

  132.         // now after all attributes are collected - in the correct hierarchy
  133.         // order - remove all unspecified entries (the ! marker)
  134.         for (Attribute a : attributes.getAll()) {
  135.             if (a.getState() == State.UNSPECIFIED)
  136.                 attributes.remove(a.getKey());
  137.         }

  138.         return attributes;
  139.     }

  140.     /**
  141.      * Merges the matching GLOBAL attributes for an entry path.
  142.      *
  143.      * @param entryPath
  144.      *            the path to test. The path must be relative to this attribute
  145.      *            node's own repository path, and in repository path format
  146.      *            (uses '/' and not '\').
  147.      * @param isDirectory
  148.      *            true if the target item is a directory.
  149.      * @param result
  150.      *            that will hold the attributes matching this entry path. This
  151.      *            method will NOT override any existing entry in attributes.
  152.      */
  153.     private void mergeGlobalAttributes(String entryPath, boolean isDirectory,
  154.             Attributes result) {
  155.         mergeAttributes(globalNode, entryPath, isDirectory, result);
  156.     }

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

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

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

  253.     /**
  254.      * Expand a macro
  255.      *
  256.      * @param attr
  257.      *            a {@link org.eclipse.jgit.attributes.Attribute}
  258.      * @param result
  259.      *            contains the (recursive) expanded and merged macro attributes
  260.      *            including the attribute iself
  261.      */
  262.     protected void expandMacro(Attribute attr, Attributes result) {
  263.         // loop detection = exists check
  264.         if (result.containsKey(attr.getKey()))
  265.             return;

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

  268.         List<Attribute> expansion = expansions.get(attr.getKey());
  269.         if (expansion == null) {
  270.             return;
  271.         }
  272.         switch (attr.getState()) {
  273.         case UNSET: {
  274.             for (Attribute e : expansion) {
  275.                 switch (e.getState()) {
  276.                 case SET:
  277.                     expandMacro(new Attribute(e.getKey(), State.UNSET), result);
  278.                     break;
  279.                 case UNSET:
  280.                     expandMacro(new Attribute(e.getKey(), State.SET), result);
  281.                     break;
  282.                 case UNSPECIFIED:
  283.                     expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
  284.                             result);
  285.                     break;
  286.                 case CUSTOM:
  287.                 default:
  288.                     expandMacro(e, result);
  289.                 }
  290.             }
  291.             break;
  292.         }
  293.         case CUSTOM: {
  294.             for (Attribute e : expansion) {
  295.                 switch (e.getState()) {
  296.                 case SET:
  297.                 case UNSET:
  298.                 case UNSPECIFIED:
  299.                     expandMacro(e, result);
  300.                     break;
  301.                 case CUSTOM:
  302.                 default:
  303.                     expandMacro(new Attribute(e.getKey(), attr.getValue()),
  304.                             result);
  305.                 }
  306.             }
  307.             break;
  308.         }
  309.         case UNSPECIFIED: {
  310.             for (Attribute e : expansion) {
  311.                 expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
  312.                         result);
  313.             }
  314.             break;
  315.         }
  316.         case SET:
  317.         default:
  318.             for (Attribute e : expansion) {
  319.                 expandMacro(e, result);
  320.             }
  321.             break;
  322.         }
  323.     }

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

  379.         return attributesNode;
  380.     }

  381.     private static <T extends AbstractTreeIterator> T parentOf(@Nullable T node) {
  382.         if(node==null) return null;
  383.         @SuppressWarnings("unchecked")
  384.         Class<T> type = (Class<T>) node.getClass();
  385.         AbstractTreeIterator parent = node.parent;
  386.         if (type.isInstance(parent)) {
  387.             return type.cast(parent);
  388.         }
  389.         return null;
  390.     }

  391.     private static <T extends AbstractTreeIterator> T rootOf(
  392.             @Nullable T node) {
  393.         if(node==null) return null;
  394.         AbstractTreeIterator t=node;
  395.         while (t!= null && t.parent != null) {
  396.             t= t.parent;
  397.         }
  398.         @SuppressWarnings("unchecked")
  399.         Class<T> type = (Class<T>) node.getClass();
  400.         if (type.isInstance(t)) {
  401.             return type.cast(t);
  402.         }
  403.         return null;
  404.     }

  405. }