diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java new file mode 100644 index 0000000000..d8857f5192 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> 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} + * <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 Supplier<CanonicalTreeParser> attributesTree; + + 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 + * 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<CanonicalTreeParser> 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<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 + * 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 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; + } + +} |