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 }