blob: 8d928e3749128101492c1f86d8712bf566ca88d3 [file] [log] [blame]
/*
* Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
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 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 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 AttributesNode globalNode;
private final AttributesNode infoNode;
private final Map<String, List<Attribute>> expansions = new HashMap<>();
/**
* Create an {@link AttributesHandler} with default rules as well as merged
* rules from global, info and worktree root attributes
*
* @param treeWalk
* @throws IOException
*/
public AttributesHandler(TreeWalk treeWalk) throws IOException {
this.treeWalk = treeWalk;
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(treeWalk
.getTree(CanonicalTreeParser.class)));
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 TreeWalk#getAttributes()}
*
* @return the {@link Attributes} for the current path represented by the
* {@link TreeWalk}
* @throws 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),
treeWalk.getTree(CanonicalTreeParser.class),
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);
}
}
}
}
/**
* @param attr
* @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;
}
}