aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java
diff options
context:
space:
mode:
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.java448
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;
+ }
+
+}