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.WorkingTreeIterator;
58  import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
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 {@link TreeWalk}
65   * <p>
66   * The implementation is based on the specifications in
67   * http://git-scm.com/docs/gitattributes
68   *
69   * @since 4.3
70   */
71  public class AttributesHandler {
72  	private static final String MACRO_PREFIX = "[attr]"; //$NON-NLS-1$
73  
74  	private static final String BINARY_RULE_KEY = "binary"; //$NON-NLS-1$
75  
76  	/**
77  	 * This is the default <b>binary</b> rule that is present in any git folder
78  	 * <code>[attr]binary -diff -merge -text</code>
79  	 */
80  	private static final List<Attribute> BINARY_RULE_ATTRIBUTES = new AttributesRule(
81  			MACRO_PREFIX + BINARY_RULE_KEY, "-diff -merge -text") //$NON-NLS-1$
82  					.getAttributes();
83  
84  	private final TreeWalk treeWalk;
85  
86  	private final AttributesNode globalNode;
87  
88  	private final AttributesNode infoNode;
89  
90  	private final Map<String, List<Attribute>> expansions = new HashMap<>();
91  
92  	/**
93  	 * Create an {@link AttributesHandler} with default rules as well as merged
94  	 * rules from global, info and worktree root attributes
95  	 *
96  	 * @param treeWalk
97  	 * @throws IOException
98  	 */
99  	public AttributesHandler(TreeWalk treeWalk) throws IOException {
100 		this.treeWalk = treeWalk;
101 		AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider();
102 		this.globalNode = attributesNodeProvider != null
103 				? attributesNodeProvider.getGlobalAttributesNode() : null;
104 		this.infoNode = attributesNodeProvider != null
105 				? attributesNodeProvider.getInfoAttributesNode() : null;
106 
107 		AttributesNode rootNode = attributesNode(treeWalk,
108 				rootOf(
109 						treeWalk.getTree(WorkingTreeIterator.class)),
110 				rootOf(
111 						treeWalk.getTree(DirCacheIterator.class)),
112 				rootOf(treeWalk
113 						.getTree(CanonicalTreeParser.class)));
114 
115 		expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES);
116 		for (AttributesNode node : new AttributesNode[] { globalNode, rootNode,
117 				infoNode }) {
118 			if (node == null) {
119 				continue;
120 			}
121 			for (AttributesRule rule : node.getRules()) {
122 				if (rule.getPattern().startsWith(MACRO_PREFIX)) {
123 					expansions.put(rule.getPattern()
124 							.substring(MACRO_PREFIX.length()).trim(),
125 							rule.getAttributes());
126 				}
127 			}
128 		}
129 	}
130 
131 	/**
132 	 * see {@link TreeWalk#getAttributes()}
133 	 *
134 	 * @return the {@link Attributes} for the current path represented by the
135 	 *         {@link TreeWalk}
136 	 * @throws IOException
137 	 */
138 	public Attributes getAttributes() throws IOException {
139 		String entryPath = treeWalk.getPathString();
140 		boolean isDirectory = (treeWalk.getFileMode() == FileMode.TREE);
141 		Attributes attributes = new Attributes();
142 
143 		// Gets the info attributes
144 		mergeInfoAttributes(entryPath, isDirectory, attributes);
145 
146 		// Gets the attributes located on the current entry path
147 		mergePerDirectoryEntryAttributes(entryPath, isDirectory,
148 				treeWalk.getTree(WorkingTreeIterator.class),
149 				treeWalk.getTree(DirCacheIterator.class),
150 				treeWalk.getTree(CanonicalTreeParser.class),
151 				attributes);
152 
153 		// Gets the attributes located in the global attribute file
154 		mergeGlobalAttributes(entryPath, isDirectory, attributes);
155 
156 		// now after all attributes are collected - in the correct hierarchy
157 		// order - remove all unspecified entries (the ! marker)
158 		for (Attribute a : attributes.getAll()) {
159 			if (a.getState() == State.UNSPECIFIED)
160 				attributes.remove(a.getKey());
161 		}
162 
163 		return attributes;
164 	}
165 
166 	/**
167 	 * Merges the matching GLOBAL attributes for an entry path.
168 	 *
169 	 * @param entryPath
170 	 *            the path to test. The path must be relative to this attribute
171 	 *            node's own repository path, and in repository path format
172 	 *            (uses '/' and not '\').
173 	 * @param isDirectory
174 	 *            true if the target item is a directory.
175 	 * @param result
176 	 *            that will hold the attributes matching this entry path. This
177 	 *            method will NOT override any existing entry in attributes.
178 	 */
179 	private void mergeGlobalAttributes(String entryPath, boolean isDirectory,
180 			Attributes result) {
181 		mergeAttributes(globalNode, entryPath, isDirectory, result);
182 	}
183 
184 	/**
185 	 * Merges the matching INFO attributes for an entry path.
186 	 *
187 	 * @param entryPath
188 	 *            the path to test. The path must be relative to this attribute
189 	 *            node's own repository path, and in repository path format
190 	 *            (uses '/' and not '\').
191 	 * @param isDirectory
192 	 *            true if the target item is a directory.
193 	 * @param result
194 	 *            that will hold the attributes matching this entry path. This
195 	 *            method will NOT override any existing entry in attributes.
196 	 */
197 	private void mergeInfoAttributes(String entryPath, boolean isDirectory,
198 			Attributes result) {
199 		mergeAttributes(infoNode, entryPath, isDirectory, result);
200 	}
201 
202 	/**
203 	 * Merges the matching working directory attributes for an entry path.
204 	 *
205 	 * @param entryPath
206 	 *            the path to test. The path must be relative to this attribute
207 	 *            node's own repository path, and in repository path format
208 	 *            (uses '/' and not '\').
209 	 * @param isDirectory
210 	 *            true if the target item is a directory.
211 	 * @param workingTreeIterator
212 	 * @param dirCacheIterator
213 	 * @param otherTree
214 	 * @param result
215 	 *            that will hold the attributes matching this entry path. This
216 	 *            method will NOT override any existing entry in attributes.
217 	 * @throws IOException
218 	 */
219 	private void mergePerDirectoryEntryAttributes(String entryPath,
220 			boolean isDirectory,
221 			@Nullable WorkingTreeIterator workingTreeIterator,
222 			@Nullable DirCacheIterator dirCacheIterator,
223 			@Nullable CanonicalTreeParser otherTree, Attributes result)
224 					throws IOException {
225 		// Prevents infinite recurrence
226 		if (workingTreeIterator != null || dirCacheIterator != null
227 				|| otherTree != null) {
228 			AttributesNode attributesNode = attributesNode(
229 					treeWalk, workingTreeIterator, dirCacheIterator, otherTree);
230 			if (attributesNode != null) {
231 				mergeAttributes(attributesNode, entryPath, isDirectory, result);
232 			}
233 			mergePerDirectoryEntryAttributes(entryPath, 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 	 * @param attr
280 	 * @param result
281 	 *            contains the (recursive) expanded and merged macro attributes
282 	 *            including the attribute iself
283 	 */
284 	protected void expandMacro(Attribute attr, Attributes result) {
285 		// loop detection = exists check
286 		if (result.containsKey(attr.getKey()))
287 			return;
288 
289 		// also add macro to result set, same does native git
290 		result.put(attr);
291 
292 		List<Attribute> expansion = expansions.get(attr.getKey());
293 		if (expansion == null) {
294 			return;
295 		}
296 		switch (attr.getState()) {
297 		case UNSET: {
298 			for (Attribute e : expansion) {
299 				switch (e.getState()) {
300 				case SET:
301 					expandMacro(new Attribute(e.getKey(), State.UNSET), result);
302 					break;
303 				case UNSET:
304 					expandMacro(new Attribute(e.getKey(), State.SET), result);
305 					break;
306 				case UNSPECIFIED:
307 					expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
308 							result);
309 					break;
310 				case CUSTOM:
311 				default:
312 					expandMacro(e, result);
313 				}
314 			}
315 			break;
316 		}
317 		case CUSTOM: {
318 			for (Attribute e : expansion) {
319 				switch (e.getState()) {
320 				case SET:
321 				case UNSET:
322 				case UNSPECIFIED:
323 					expandMacro(e, result);
324 					break;
325 				case CUSTOM:
326 				default:
327 					expandMacro(new Attribute(e.getKey(), attr.getValue()),
328 							result);
329 				}
330 			}
331 			break;
332 		}
333 		case UNSPECIFIED: {
334 			for (Attribute e : expansion) {
335 				expandMacro(new Attribute(e.getKey(), State.UNSPECIFIED),
336 						result);
337 			}
338 			break;
339 		}
340 		case SET:
341 		default:
342 			for (Attribute e : expansion) {
343 				expandMacro(e, result);
344 			}
345 			break;
346 		}
347 	}
348 
349 	/**
350 	 * Get the {@link AttributesNode} for the current entry.
351 	 * <p>
352 	 * This method implements the fallback mechanism between the index and the
353 	 * working tree depending on the operation type
354 	 * </p>
355 	 *
356 	 * @param treeWalk
357 	 * @param workingTreeIterator
358 	 * @param dirCacheIterator
359 	 * @param otherTree
360 	 * @return a {@link AttributesNode} of the current entry,
361 	 *         {@link NullPointerException} otherwise.
362 	 * @throws IOException
363 	 *             It raises an {@link IOException} if a problem appears while
364 	 *             parsing one on the attributes file.
365 	 */
366 	private static AttributesNode attributesNode(TreeWalk treeWalk,
367 			@Nullable WorkingTreeIterator workingTreeIterator,
368 			@Nullable DirCacheIterator dirCacheIterator,
369 			@Nullable CanonicalTreeParser otherTree) throws IOException {
370 		AttributesNode attributesNode = null;
371 		switch (treeWalk.getOperationType()) {
372 		case CHECKIN_OP:
373 			if (workingTreeIterator != null) {
374 				attributesNode = workingTreeIterator.getEntryAttributesNode();
375 			}
376 			if (attributesNode == null && dirCacheIterator != null) {
377 				attributesNode = dirCacheIterator
378 						.getEntryAttributesNode(treeWalk.getObjectReader());
379 			}
380 			if (attributesNode == null && otherTree != null) {
381 				attributesNode = otherTree
382 						.getEntryAttributesNode(treeWalk.getObjectReader());
383 			}
384 			break;
385 		case CHECKOUT_OP:
386 			if (otherTree != null) {
387 				attributesNode = otherTree
388 						.getEntryAttributesNode(treeWalk.getObjectReader());
389 			}
390 			if (attributesNode == null && dirCacheIterator != null) {
391 				attributesNode = dirCacheIterator
392 						.getEntryAttributesNode(treeWalk.getObjectReader());
393 			}
394 			if (attributesNode == null && workingTreeIterator != null) {
395 				attributesNode = workingTreeIterator.getEntryAttributesNode();
396 			}
397 			break;
398 		default:
399 			throw new IllegalStateException(
400 					"The only supported operation types are:" //$NON-NLS-1$
401 							+ OperationType.CHECKIN_OP + "," //$NON-NLS-1$
402 							+ OperationType.CHECKOUT_OP);
403 		}
404 
405 		return attributesNode;
406 	}
407 
408 	private static <T extends AbstractTreeIterator> T parentOf(@Nullable T node) {
409 		if(node==null) return null;
410 		@SuppressWarnings("unchecked")
411 		Class<T> type = (Class<T>) node.getClass();
412 		AbstractTreeIterator parent = node.parent;
413 		if (type.isInstance(parent)) {
414 			return type.cast(parent);
415 		}
416 		return null;
417 	}
418 
419 	private static <T extends AbstractTreeIterator> T rootOf(
420 			@Nullable T node) {
421 		if(node==null) return null;
422 		AbstractTreeIterator t=node;
423 		while (t!= null && t.parent != null) {
424 			t= t.parent;
425 		}
426 		@SuppressWarnings("unchecked")
427 		Class<T> type = (Class<T>) node.getClass();
428 		if (type.isInstance(t)) {
429 			return type.cast(t);
430 		}
431 		return null;
432 	}
433 
434 }