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