View Javadoc
1   /*
2    * Copyright (C) 2008, Google Inc. 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  
11  package org.eclipse.jgit.treewalk;
12  
13  import java.io.IOException;
14  
15  import org.eclipse.jgit.annotations.Nullable;
16  import org.eclipse.jgit.errors.CorruptObjectException;
17  import org.eclipse.jgit.lib.FileMode;
18  import org.eclipse.jgit.lib.ObjectReader;
19  import org.eclipse.jgit.lib.Repository;
20  
21  /**
22   * Specialized TreeWalk to detect directory-file (D/F) name conflicts.
23   * <p>
24   * Due to the way a Git tree is organized the standard
25   * {@link org.eclipse.jgit.treewalk.TreeWalk} won't easily find a D/F conflict
26   * when merging two or more trees together. In the standard TreeWalk the file
27   * will be returned first, and then much later the directory will be returned.
28   * This makes it impossible for the application to efficiently detect and handle
29   * the conflict.
30   * <p>
31   * Using this walk implementation causes the directory to report earlier than
32   * usual, at the same time as the non-directory entry. This permits the
33   * application to handle the D/F conflict in a single step. The directory is
34   * returned only once, so it does not get returned later in the iteration.
35   * <p>
36   * When a D/F conflict is detected
37   * {@link org.eclipse.jgit.treewalk.TreeWalk#isSubtree()} will return true and
38   * {@link org.eclipse.jgit.treewalk.TreeWalk#enterSubtree()} will recurse into
39   * the subtree, no matter which iterator originally supplied the subtree.
40   * <p>
41   * Because conflicted directories report early, using this walk implementation
42   * to populate a {@link org.eclipse.jgit.dircache.DirCacheBuilder} may cause the
43   * automatic resorting to run and fix the entry ordering.
44   * <p>
45   * This walk implementation requires more CPU to implement a look-ahead and a
46   * look-behind to merge a D/F pair together, or to skip a previously reported
47   * directory. In typical Git repositories the look-ahead cost is 0 and the
48   * look-behind doesn't trigger, as users tend not to create trees which contain
49   * both "foo" as a directory and "foo.c" as a file.
50   * <p>
51   * In the worst-case however several thousand look-ahead steps per walk step may
52   * be necessary, making the overhead quite significant. Since this worst-case
53   * should never happen this walk implementation has made the time/space tradeoff
54   * in favor of more-time/less-space, as that better suits the typical case.
55   */
56  public class NameConflictTreeWalk extends TreeWalk {
57  	private static final int TREE_MODE = FileMode.TREE.getBits();
58  
59  	/**
60  	 * True if all {@link #trees} point to entries with equal names.
61  	 *
62  	 * If at least one tree iterator point to a different name or
63  	 * reached end of the tree, the value is false.
64  	 * Note: if all iterators reached end of trees, the value is true.
65  	 */
66  	private boolean allTreesNamesMatchFastMinRef;
67  
68  	private AbstractTreeIterator dfConflict;
69  
70  	/**
71  	 * Create a new tree walker for a given repository.
72  	 *
73  	 * @param repo
74  	 *            the repository the walker will obtain data from.
75  	 */
76  	public NameConflictTreeWalk(Repository repo) {
77  		super(repo);
78  	}
79  
80  	/**
81  	 * Create a new tree walker for a given repository.
82  	 *
83  	 * @param repo
84  	 *            the repository the walker will obtain data from.
85  	 * @param or
86  	 *            the reader the walker will obtain tree data from.
87  	 * @since 4.3
88  	 */
89  	public NameConflictTreeWalk(@Nullable Repository repo, ObjectReader or) {
90  		super(repo, or);
91  	}
92  
93  	/**
94  	 * Create a new tree walker for a given repository.
95  	 *
96  	 * @param or
97  	 *            the reader the walker will obtain tree data from.
98  	 */
99  	public NameConflictTreeWalk(ObjectReader or) {
100 		super(or);
101 	}
102 
103 	@Override
104 	AbstractTreeIterator min() throws CorruptObjectException {
105 		for (;;) {
106 			final AbstractTreeIterator minRef = fastMin();
107 			if (allTreesNamesMatchFastMinRef)
108 				return minRef;
109 
110 			if (isTree(minRef)) {
111 				if (skipEntry(minRef)) {
112 					for (AbstractTreeIterator t : trees) {
113 						if (t.matches == minRef) {
114 							t.next(1);
115 							t.matches = null;
116 						}
117 					}
118 					continue;
119 				}
120 				return minRef;
121 			}
122 
123 			return combineDF(minRef);
124 		}
125 	}
126 
127 	private AbstractTreeIterator fastMin() {
128 		int i = getFirstNonEofTreeIndex();
129 		if (i == -1) {
130 			// All trees reached the end.
131 			allTreesNamesMatchFastMinRef = true;
132 			return trees[trees.length - 1];
133 		}
134 		AbstractTreeIterator minRef = trees[i];
135 		// if i > 0 then we already know that only some trees reached the end
136 		// (but not all), so it is impossible that ALL trees points to the
137 		// minRef entry.
138 		// Only if i == 0 it is still possible that all trees points to the same
139 		// minRef entry.
140 		allTreesNamesMatchFastMinRef = i == 0;
141 		boolean hasConflict = false;
142 		minRef.matches = minRef;
143 		while (++i < trees.length) {
144 			final AbstractTreeIterator t = trees[i];
145 			if (t.eof()) {
146 				allTreesNamesMatchFastMinRef = false;
147 				continue;
148 			}
149 
150 			final int cmp = t.pathCompare(minRef);
151 			if (cmp < 0) {
152 				if (allTreesNamesMatchFastMinRef && isTree(minRef) && !isTree(t)
153 						&& nameEqual(minRef, t)) {
154 					// We used to be at a tree, but now we are at a file
155 					// with the same name. Allow the file to match the
156 					// tree anyway.
157 					//
158 					t.matches = minRef;
159 					hasConflict = true;
160 				} else {
161 					allTreesNamesMatchFastMinRef = false;
162 					t.matches = t;
163 					minRef = t;
164 				}
165 			} else if (cmp == 0) {
166 				// Exact name/mode match is best.
167 				//
168 				t.matches = minRef;
169 			} else if (allTreesNamesMatchFastMinRef && isTree(t)
170 					&& !isTree(minRef) && !isGitlink(minRef)
171 					&& nameEqual(t, minRef)) {
172 				// The minimum is a file (non-tree) but the next entry
173 				// of this iterator is a tree whose name matches our file.
174 				// This is a classic D/F conflict and commonly occurs like
175 				// this, with no gaps in between the file and directory.
176 				//
177 				// Use the tree as the minimum instead (see combineDF).
178 				//
179 
180 				for (int k = 0; k < i; k++) {
181 					final AbstractTreeIterator p = trees[k];
182 					if (p.matches == minRef)
183 						p.matches = t;
184 				}
185 				t.matches = t;
186 				minRef = t;
187 				hasConflict = true;
188 			} else
189 				allTreesNamesMatchFastMinRef = false;
190 		}
191 
192 		if (hasConflict && allTreesNamesMatchFastMinRef && dfConflict == null)
193 			dfConflict = minRef;
194 		return minRef;
195 	}
196 
197 	private int getFirstNonEofTreeIndex() {
198 		for (int i = 0; i < trees.length; i++) {
199 			if (!trees[i].eof()) {
200 				return i;
201 			}
202 		}
203 		return -1;
204 	}
205 
206 	private static boolean nameEqual(final AbstractTreeIterator a,
207 			final AbstractTreeIterator b) {
208 		return a.pathCompare(b, TREE_MODE) == 0;
209 	}
210 
211 	private boolean isGitlink(AbstractTreeIterator p) {
212 		return FileMode.GITLINK.equals(p.mode);
213 	}
214 
215 	private static boolean isTree(AbstractTreeIterator p) {
216 		return FileMode.TREE.equals(p.mode);
217 	}
218 
219 	private boolean skipEntry(AbstractTreeIterator minRef)
220 			throws CorruptObjectException {
221 		// A tree D/F may have been handled earlier. We need to
222 		// not report this path if it has already been reported.
223 		//
224 		for (AbstractTreeIterator t : trees) {
225 			if (t.matches == minRef || t.first())
226 				continue;
227 
228 			int stepsBack = 0;
229 			for (;;) {
230 				stepsBack++;
231 				t.back(1);
232 
233 				final int cmp = t.pathCompare(minRef, 0);
234 				if (cmp == 0) {
235 					// We have already seen this "$path" before. Skip it.
236 					//
237 					t.next(stepsBack);
238 					return true;
239 				} else if (cmp < 0 || t.first()) {
240 					// We cannot find "$path" in t; it will never appear.
241 					//
242 					t.next(stepsBack);
243 					break;
244 				}
245 			}
246 		}
247 
248 		// We have never seen the current path before.
249 		//
250 		return false;
251 	}
252 
253 	private AbstractTreeIterator combineDF(AbstractTreeIterator minRef)
254 			throws CorruptObjectException {
255 		// Look for a possible D/F conflict forward in the tree(s)
256 		// as there may be a "$path/" which matches "$path". Make
257 		// such entries match this entry.
258 		//
259 		AbstractTreeIterator treeMatch = null;
260 		for (AbstractTreeIterator t : trees) {
261 			if (t.matches == minRef || t.eof())
262 				continue;
263 
264 			for (;;) {
265 				final int cmp = t.pathCompare(minRef, TREE_MODE);
266 				if (cmp < 0) {
267 					// The "$path/" may still appear later.
268 					//
269 					t.matchShift++;
270 					t.next(1);
271 					if (t.eof()) {
272 						t.back(t.matchShift);
273 						t.matchShift = 0;
274 						break;
275 					}
276 				} else if (cmp == 0) {
277 					// We have a conflict match here.
278 					//
279 					t.matches = minRef;
280 					treeMatch = t;
281 					break;
282 				} else {
283 					// A conflict match is not possible.
284 					//
285 					if (t.matchShift != 0) {
286 						t.back(t.matchShift);
287 						t.matchShift = 0;
288 					}
289 					break;
290 				}
291 			}
292 		}
293 
294 		// When the combineDF is called, the t.matches field stores other
295 		// entry (i.e. tree iterator) with an equal path. However, the
296 		// previous loop moves each iterator independently. As a result,
297 		// iterators which have had equals path at the start of the
298 		// method can have different paths at this point.
299 		// Reevaluate existing matches.
300 		// The NameConflictTreeWalkTest.testDF_specialFileNames test
301 		// cover this situation.
302 		for (AbstractTreeIterator t : trees) {
303 			// The previous loop doesn't touch tree iterator if it matches
304 			// minRef. Skip it here
305 			if (t.eof() || t.matches == null || t.matches == minRef)
306 				continue;
307 			// The t.pathCompare takes into account the entry type (file
308 			// or directory) and returns non-zero value if names match
309 			// but entry type don't match.
310 			// We want to keep such matches (file/directory conflict),
311 			// so reset matches only if names are not equal.
312 			if (!nameEqual(t, t.matches))
313 				t.matches = null;
314 		}
315 
316 		if (treeMatch != null) {
317 			// If we do have a conflict use one of the directory
318 			// matching iterators instead of the file iterator.
319 			// This way isSubtree is true and isRecursive works.
320 			//
321 			for (AbstractTreeIterator t : trees)
322 				if (t.matches == minRef)
323 					t.matches = treeMatch;
324 
325 			if (dfConflict == null && !isGitlink(minRef)) {
326 				dfConflict = treeMatch;
327 			}
328 
329 			return treeMatch;
330 		}
331 
332 		return minRef;
333 	}
334 
335 	@Override
336 	void popEntriesEqual() throws CorruptObjectException {
337 		final AbstractTreeIterator ch = currentHead;
338 		for (AbstractTreeIterator t : trees) {
339 			if (t.matches == ch) {
340 				if (t.matchShift == 0)
341 					t.next(1);
342 				else {
343 					t.back(t.matchShift);
344 					t.matchShift = 0;
345 				}
346 				t.matches = null;
347 			}
348 		}
349 
350 		if (ch == dfConflict)
351 			dfConflict = null;
352 	}
353 
354 	@Override
355 	void skipEntriesEqual() throws CorruptObjectException {
356 		final AbstractTreeIterator ch = currentHead;
357 		for (AbstractTreeIterator t : trees) {
358 			if (t.matches == ch) {
359 				if (t.matchShift == 0)
360 					t.skip();
361 				else {
362 					t.back(t.matchShift);
363 					t.matchShift = 0;
364 				}
365 				t.matches = null;
366 			}
367 		}
368 
369 		if (ch == dfConflict)
370 			dfConflict = null;
371 	}
372 
373 	@Override
374 	void stopWalk() throws IOException {
375 		if (!needsStopWalk()) {
376 			return;
377 		}
378 
379 		// Name conflicts make aborting early difficult. Multiple paths may
380 		// exist between the file and directory versions of a name. To ensure
381 		// the directory version is skipped over (as it was previously visited
382 		// during the file version step) requires popping up the stack and
383 		// finishing out each subtree that the walker dove into. Siblings in
384 		// parents do not need to be recursed into, bounding the cost.
385 		for (;;) {
386 			AbstractTreeIterator t = min();
387 			if (t.eof()) {
388 				if (depth > 0) {
389 					exitSubtree();
390 					popEntriesEqual();
391 					continue;
392 				}
393 				return;
394 			}
395 			currentHead = t;
396 			skipEntriesEqual();
397 		}
398 	}
399 
400 	private boolean needsStopWalk() {
401 		for (AbstractTreeIterator t : trees) {
402 			if (t.needsStopWalk()) {
403 				return true;
404 			}
405 		}
406 		return false;
407 	}
408 
409 	/**
410 	 * True if the current entry is covered by a directory/file conflict.
411 	 *
412 	 * This means that for some prefix of the current entry's path, this walk
413 	 * has detected a directory/file conflict. Also true if the current entry
414 	 * itself is a directory/file conflict.
415 	 *
416 	 * Example: If this TreeWalk points to foo/bar/a.txt and this method returns
417 	 * true then you know that either for path foo or for path foo/bar files and
418 	 * folders were detected.
419 	 *
420 	 * @return <code>true</code> if the current entry is covered by a
421 	 *         directory/file conflict, <code>false</code> otherwise
422 	 */
423 	public boolean isDirectoryFileConflict() {
424 		return dfConflict != null;
425 	}
426 }