AttributesHandler.java
/*
* 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
* @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
* @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
*/
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
* @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;
}
}