View Javadoc
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  
12  import java.io.IOException;
13  import java.util.HashMap;
14  import java.util.List;
15  import java.util.ListIterator;
16  import java.util.Map;
17  import java.util.function.Supplier;
18  
19  import org.eclipse.jgit.annotations.Nullable;
20  import org.eclipse.jgit.attributes.Attribute.State;
21  import org.eclipse.jgit.dircache.DirCacheIterator;
22  import org.eclipse.jgit.lib.FileMode;
23  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
24  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
25  import org.eclipse.jgit.treewalk.TreeWalk;
26  import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
27  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
28  
29  /**
30   * The attributes handler knows how to retrieve, parse and merge attributes from
31   * the various gitattributes files. Furthermore it collects and expands macro
32   * expressions. The method {@link #getAttributes()} yields the ready processed
33   * attributes for the current path represented by the
34   * {@link org.eclipse.jgit.treewalk.TreeWalk}
35   * <p>
36   * The implementation is based on the specifications in
37   * http://git-scm.com/docs/gitattributes
38   *
39   * @since 4.3
40   */
41  public class AttributesHandler {
42  	private static final String MACRO_PREFIX = "[attr]"; //$NON-NLS-1$
43  
44  	private static final String BINARY_RULE_KEY = "binary"; //$NON-NLS-1$
45  
46  	/**
47  	 * This is the default <b>binary</b> rule that is present in any git folder
48  	 * <code>[attr]binary -diff -merge -text</code>
49  	 */
50  	private static final List<Attribute> BINARY_RULE_ATTRIBUTES = new AttributesRule(
51  			MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$
52  					.getAttributes();
53  
54  	private final TreeWalk treeWalk;
55  
56  	private final Supplier<CanonicalTreeParser> attributesTree;
57  
58  	private final AttributesNode globalNode;
59  
60  	private final AttributesNode infoNode;
61  
62  	private final Map<String, List<Attribute>> expansions = new HashMap<>();
63  
64  	/**
65  	 * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with
66  	 * default rules as well as merged rules from global, info and worktree root
67  	 * attributes
68  	 *
69  	 * @param treeWalk
70  	 *            a {@link org.eclipse.jgit.treewalk.TreeWalk}
71  	 * @throws java.io.IOException
72  	 * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)}
73  	 *             instead
74  	 */
75  	@Deprecated
76  	public AttributesHandler(TreeWalk treeWalk) throws IOException {
77  		this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class));
78  	}
79  
80  	/**
81  	 * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with
82  	 * default rules as well as merged rules from global, info and worktree root
83  	 * attributes
84  	 *
85  	 * @param treeWalk
86  	 *            a {@link org.eclipse.jgit.treewalk.TreeWalk}
87  	 * @param attributesTree
88  	 *            the tree to read .gitattributes from
89  	 * @throws java.io.IOException
90  	 * @since 6.1
91  	 */
92  	public AttributesHandler(TreeWalk treeWalk,
93  			Supplier<CanonicalTreeParser> attributesTree) throws IOException {
94  		this.treeWalk = treeWalk;
95  		this.attributesTree = attributesTree;
96  		AttributesNodeProvider attributesNodeProvider = treeWalk
97  				.getAttributesNodeProvider();
98  		this.globalNode = attributesNodeProvider != null
99  				? attributesNodeProvider.getGlobalAttributesNode() : null;
100 		this.infoNode = attributesNodeProvider != null
101 				? attributesNodeProvider.getInfoAttributesNode() : null;
102 
103 		AttributesNode rootNode = attributesNode(treeWalk,
104 				rootOf(treeWalk.getTree(WorkingTreeIterator.class)),
105 				rootOf(treeWalk.getTree(DirCacheIterator.class)),
106 				rootOf(attributesTree.get()));
107 
108 		expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES);
109 		for (AttributesNode node : new AttributesNode[] { globalNode, rootNode,
110 				infoNode }) {
111 			if (node == null) {
112 				continue;
113 			}
114 			for (AttributesRule rule : node.getRules()) {
115 				if (rule.getPattern().startsWith(MACRO_PREFIX)) {
116 					expansions.put(rule.getPattern()
117 							.substring(MACRO_PREFIX.length()).trim(),
118 							rule.getAttributes());
119 				}
120 			}
121 		}
122 	}
123 
124 	/**
125 	 * See {@link org.eclipse.jgit.treewalk.TreeWalk#getAttributes()}
126 	 *
127 	 * @return the {@link org.eclipse.jgit.attributes.Attributes} for the
128 	 *         current path represented by the
129 	 *         {@link org.eclipse.jgit.treewalk.TreeWalk}
130 	 * @throws java.io.IOException
131 	 */
132 	public Attributes getAttributes() throws IOException {
133 		String entryPath = treeWalk.getPathString();
134 		boolean isDirectory = (treeWalk.getFileMode() == FileMode.TREE);
135 		Attributes attributes = new Attributes();
136 
137 		// Gets the info attributes
138 		mergeInfoAttributes(entryPath, isDirectory, attributes);
139 
140 		// Gets the attributes located on the current entry path
141 		mergePerDirectoryEntryAttributes(entryPath, entryPath.lastIndexOf('/'),
142 				isDirectory,
143 				treeWalk.getTree(WorkingTreeIterator.class),
144 				treeWalk.getTree(DirCacheIterator.class),
145 				attributesTree.get(),
146 				attributes);
147 
148 		// Gets the attributes located in the global attribute file
149 		mergeGlobalAttributes(entryPath, isDirectory, attributes);
150 
151 		// now after all attributes are collected - in the correct hierarchy
152 		// order - remove all unspecified entries (the ! marker)
153 		for (Attribute a : attributes.getAll()) {
154 			if (a.getState() == State.UNSPECIFIED)
155 				attributes.remove(a.getKey());
156 		}
157 
158 		return attributes;
159 	}
160 
161 	/**
162 	 * Merges the matching GLOBAL attributes for an entry path.
163 	 *
164 	 * @param entryPath
165 	 *            the path to test. The path must be relative to this attribute
166 	 *            node's own repository path, and in repository path format
167 	 *            (uses '/' and not '\').
168 	 * @param isDirectory
169 	 *            true if the target item is a directory.
170 	 * @param result
171 	 *            that will hold the attributes matching this entry path. This
172 	 *            method will NOT override any existing entry in attributes.
173 	 */
174 	private void mergeGlobalAttributes(String entryPath, boolean isDirectory,
175 			Attributes result) {
176 		mergeAttributes(globalNode, entryPath, isDirectory, result);
177 	}
178 
179 	/**
180 	 * Merges the matching INFO attributes for an entry path.
181 	 *
182 	 * @param entryPath
183 	 *            the path to test. The path must be relative to this attribute
184 	 *            node's own repository path, and in repository path format
185 	 *            (uses '/' and not '\').
186 	 * @param isDirectory
187 	 *            true if the target item is a directory.
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 	 */
192 	private void mergeInfoAttributes(String entryPath, boolean isDirectory,
193 			Attributes result) {
194 		mergeAttributes(infoNode, entryPath, isDirectory, result);
195 	}
196 
197 	/**
198 	 * Merges the matching working directory attributes for an entry path.
199 	 *
200 	 * @param entryPath
201 	 *            the path to test. The path must be relative to this attribute
202 	 *            node's own repository path, and in repository path format
203 	 *            (uses '/' and not '\').
204 	 * @param nameRoot
205 	 *            index of the '/' preceeding the current level, or -1 if none
206 	 * @param isDirectory
207 	 *            true if the target item is a directory.
208 	 * @param workingTreeIterator
209 	 * @param dirCacheIterator
210 	 * @param otherTree
211 	 * @param result
212 	 *            that will hold the attributes matching this entry path. This
213 	 *            method will NOT override any existing entry in attributes.
214 	 * @throws IOException
215 	 */
216 	private void mergePerDirectoryEntryAttributes(String entryPath,
217 			int nameRoot, boolean isDirectory,
218 			@Nullable WorkingTreeIterator workingTreeIterator,
219 			@Nullable DirCacheIterator dirCacheIterator,
220 			@Nullable CanonicalTreeParser otherTree, Attributes result)
221 					throws IOException {
222 		// Prevents infinite recurrence
223 		if (workingTreeIterator != null || dirCacheIterator != null
224 				|| otherTree != null) {
225 			AttributesNode attributesNode = attributesNode(
226 					treeWalk, workingTreeIterator, dirCacheIterator, otherTree);
227 			if (attributesNode != null) {
228 				mergeAttributes(attributesNode,
229 						entryPath.substring(nameRoot + 1), isDirectory,
230 						result);
231 			}
232 			mergePerDirectoryEntryAttributes(entryPath,
233 					entryPath.lastIndexOf('/', nameRoot - 1), isDirectory,
234 					parentOf(workingTreeIterator), parentOf(dirCacheIterator),
235 					parentOf(otherTree), result);
236 		}
237 	}
238 
239 	/**
240 	 * Merges the matching node attributes for an entry path.
241 	 *
242 	 * @param node
243 	 *            the node to scan for matches to entryPath
244 	 * @param entryPath
245 	 *            the path to test. The path must be relative to this attribute
246 	 *            node's own repository path, and in repository path format
247 	 *            (uses '/' and not '\').
248 	 * @param isDirectory
249 	 *            true if the target item is a directory.
250 	 * @param result
251 	 *            that will hold the attributes matching this entry path. This
252 	 *            method will NOT override any existing entry in attributes.
253 	 */
254 	protected void mergeAttributes(@Nullable AttributesNode node,
255 			String entryPath,
256 			boolean isDirectory, Attributes result) {
257 		if (node == null)
258 			return;
259 		List<AttributesRule> rules = node.getRules();
260 		// Parse rules in the reverse order that they were read since the last
261 		// entry should be used
262 		ListIterator<AttributesRule> ruleIterator = rules
263 				.listIterator(rules.size());
264 		while (ruleIterator.hasPrevious()) {
265 			AttributesRule rule = ruleIterator.previous();
266 			if (rule.isMatch(entryPath, isDirectory)) {
267 				ListIterator<Attribute> attributeIte = rule.getAttributes()
268 						.listIterator(rule.getAttributes().size());
269 				// Parses the attributes in the reverse order that they were
270 				// read since the last entry should be used
271 				while (attributeIte.hasPrevious()) {
272 					expandMacro(attributeIte.previous(), result);
273 				}
274 			}
275 		}
276 	}
277 
278 	/**
279 	 * Expand a macro
280 	 *
281 	 * @param attr
282 	 *            a {@link org.eclipse.jgit.attributes.Attribute}
283 	 * @param result
284 	 *            contains the (recursive) expanded and merged macro attributes
285 	 *            including the attribute iself
286 	 */
287 	protected void expandMacro(Attribute attr, Attributes result) {
288 		// loop detection = exists check
289 		if (result.containsKey(attr.getKey()))
290 			return;
291 
292 		// also add macro to result set, same does native git
293 		result.put(attr);
294 
295 		List<Attribute> expansion = expansions.get(attr.getKey());
296 		if (expansion == null) {
297 			return;
298 		}
299 		switch (attr.getState()) {
300 		case UNSET: {
301 			for (Attribute e : expansion) {
302 				switch (e.getState()) {
303 				case SET:
304 					expandMacro(new Attribute(e.getKey(), State.UNSET), result);
305 					break;
306 				case UNSET:
307 					expandMacro(new Attribute(e.getKey(), State.SET), result);
308 					break;
309 				case UNSPECIFIED:
310 					expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
311 							result);
312 					break;
313 				case CUSTOM:
314 				default:
315 					expandMacro(e, result);
316 				}
317 			}
318 			break;
319 		}
320 		case CUSTOM: {
321 			for (Attribute e : expansion) {
322 				switch (e.getState()) {
323 				case SET:
324 				case UNSET:
325 				case UNSPECIFIED:
326 					expandMacro(e, result);
327 					break;
328 				case CUSTOM:
329 				default:
330 					expandMacro(new Attribute(e.getKey(), attr.getValue()),
331 							result);
332 				}
333 			}
334 			break;
335 		}
336 		case UNSPECIFIED: {
337 			for (Attribute e : expansion) {
338 				expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
339 						result);
340 			}
341 			break;
342 		}
343 		case SET:
344 		default:
345 			for (Attribute e : expansion) {
346 				expandMacro(e, result);
347 			}
348 			break;
349 		}
350 	}
351 
352 	/**
353 	 * Get the {@link AttributesNode} for the current entry.
354 	 * <p>
355 	 * This method implements the fallback mechanism between the index and the
356 	 * working tree depending on the operation type
357 	 * </p>
358 	 *
359 	 * @param treeWalk
360 	 * @param workingTreeIterator
361 	 * @param dirCacheIterator
362 	 * @param otherTree
363 	 * @return a {@link AttributesNode} of the current entry,
364 	 *         {@link NullPointerException} otherwise.
365 	 * @throws IOException
366 	 *             It raises an {@link IOException} if a problem appears while
367 	 *             parsing one on the attributes file.
368 	 */
369 	private static AttributesNode attributesNode(TreeWalk treeWalk,
370 			@Nullable WorkingTreeIterator workingTreeIterator,
371 			@Nullable DirCacheIterator dirCacheIterator,
372 			@Nullable CanonicalTreeParser otherTree) throws IOException {
373 		AttributesNode attributesNode = null;
374 		switch (treeWalk.getOperationType()) {
375 		case CHECKIN_OP:
376 			if (workingTreeIterator != null) {
377 				attributesNode = workingTreeIterator.getEntryAttributesNode();
378 			}
379 			if (attributesNode == null && dirCacheIterator != null) {
380 				attributesNode = dirCacheIterator
381 						.getEntryAttributesNode(treeWalk.getObjectReader());
382 			}
383 			if (attributesNode == null && otherTree != null) {
384 				attributesNode = otherTree
385 						.getEntryAttributesNode(treeWalk.getObjectReader());
386 			}
387 			break;
388 		case CHECKOUT_OP:
389 			if (otherTree != null) {
390 				attributesNode = otherTree
391 						.getEntryAttributesNode(treeWalk.getObjectReader());
392 			}
393 			if (attributesNode == null && dirCacheIterator != null) {
394 				attributesNode = dirCacheIterator
395 						.getEntryAttributesNode(treeWalk.getObjectReader());
396 			}
397 			if (attributesNode == null && workingTreeIterator != null) {
398 				attributesNode = workingTreeIterator.getEntryAttributesNode();
399 			}
400 			break;
401 		default:
402 			throw new IllegalStateException(
403 					"The only supported operation types are:" //$NON-NLS-1$
404 							+ OperationType.CHECKIN_OP + "," //$NON-NLS-1$
405 							+ OperationType.CHECKOUT_OP);
406 		}
407 
408 		return attributesNode;
409 	}
410 
411 	private static <T extends AbstractTreeIterator> T parentOf(@Nullable T node) {
412 		if(node==null) return null;
413 		@SuppressWarnings("unchecked")
414 		Class<T> type = (Class<T>) node.getClass();
415 		AbstractTreeIterator parent = node.parent;
416 		if (type.isInstance(parent)) {
417 			return type.cast(parent);
418 		}
419 		return null;
420 	}
421 
422 	private static <T extends AbstractTreeIterator> T rootOf(
423 			@Nullable T node) {
424 		if(node==null) return null;
425 		AbstractTreeIterator t=node;
426 		while (t!= null && t.parent != null) {
427 			t= t.parent;
428 		}
429 		@SuppressWarnings("unchecked")
430 		Class<T> type = (Class<T>) node.getClass();
431 		if (type.isInstance(t)) {
432 			return type.cast(t);
433 		}
434 		return null;
435 	}
436 
437 }