View Javadoc
1   /*
2    * Copyright (C) 2010, Red Hat Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.ignore;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.eclipse.jgit.junit.Assert.assertEquals;
47  import static org.junit.Assert.assertEquals;
48  import static org.junit.Assert.assertFalse;
49  import static org.junit.Assert.assertNotNull;
50  import static org.junit.Assert.assertTrue;
51  
52  import java.io.ByteArrayInputStream;
53  import java.io.File;
54  import java.io.IOException;
55  import java.io.InputStream;
56  import java.util.ArrayList;
57  import java.util.Arrays;
58  
59  import org.eclipse.jgit.ignore.IgnoreNode.MatchResult;
60  import org.eclipse.jgit.junit.RepositoryTestCase;
61  import org.eclipse.jgit.lib.FileMode;
62  import org.eclipse.jgit.treewalk.FileTreeIterator;
63  import org.eclipse.jgit.treewalk.TreeWalk;
64  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
65  import org.eclipse.jgit.util.FileUtils;
66  import org.eclipse.jgit.util.SystemReader;
67  import org.junit.Test;
68  
69  /**
70   * Tests ignore node behavior on the local filesystem.
71   */
72  public class IgnoreNodeTest extends RepositoryTestCase {
73  	private static final FileMode D = FileMode.TREE;
74  
75  	private static final FileMode F = FileMode.REGULAR_FILE;
76  
77  	private static final boolean ignored = true;
78  
79  	private static final boolean tracked = false;
80  
81  	private TreeWalk walk;
82  
83  	@Test
84  	public void testRules() throws IOException {
85  		writeIgnoreFile(".git/info/exclude", "*~", "/out");
86  
87  		writeIgnoreFile(".gitignore", "*.o", "/config");
88  		writeTrashFile("config/secret", "");
89  		writeTrashFile("mylib.c", "");
90  		writeTrashFile("mylib.c~", "");
91  		writeTrashFile("mylib.o", "");
92  
93  		writeTrashFile("out/object/foo.exe", "");
94  		writeIgnoreFile("src/config/.gitignore", "lex.out");
95  		writeTrashFile("src/config/lex.out", "");
96  		writeTrashFile("src/config/config.c", "");
97  		writeTrashFile("src/config/config.c~", "");
98  		writeTrashFile("src/config/old/lex.out", "");
99  
100 		beginWalk();
101 		assertEntry(F, tracked, ".gitignore");
102 		assertEntry(D, ignored, "config");
103 		assertEntry(F, ignored, "config/secret");
104 		assertEntry(F, tracked, "mylib.c");
105 		assertEntry(F, ignored, "mylib.c~");
106 		assertEntry(F, ignored, "mylib.o");
107 
108 		assertEntry(D, ignored, "out");
109 		assertEntry(D, ignored, "out/object");
110 		assertEntry(F, ignored, "out/object/foo.exe");
111 
112 		assertEntry(D, tracked, "src");
113 		assertEntry(D, tracked, "src/config");
114 		assertEntry(F, tracked, "src/config/.gitignore");
115 		assertEntry(F, tracked, "src/config/config.c");
116 		assertEntry(F, ignored, "src/config/config.c~");
117 		assertEntry(F, ignored, "src/config/lex.out");
118 		assertEntry(D, tracked, "src/config/old");
119 		assertEntry(F, ignored, "src/config/old/lex.out");
120 		endWalk();
121 	}
122 
123 	@Test
124 	public void testNegation() throws IOException {
125 		// ignore all *.o files and ignore all "d" directories
126 		writeIgnoreFile(".gitignore", "*.o", "d");
127 
128 		// negate "ignore" for a/b/keep.o file only
129 		writeIgnoreFile("src/a/b/.gitignore", "!keep.o");
130 		writeTrashFile("src/a/b/keep.o", "");
131 		writeTrashFile("src/a/b/nothere.o", "");
132 
133 		// negate "ignore" for "d"
134 		writeIgnoreFile("src/c/.gitignore", "!d");
135 		// negate "ignore" for c/d/keep.o file only
136 		writeIgnoreFile("src/c/d/.gitignore", "!keep.o");
137 		writeTrashFile("src/c/d/keep.o", "");
138 		writeTrashFile("src/c/d/nothere.o", "");
139 
140 		beginWalk();
141 		assertEntry(F, tracked, ".gitignore");
142 		assertEntry(D, tracked, "src");
143 		assertEntry(D, tracked, "src/a");
144 		assertEntry(D, tracked, "src/a/b");
145 		assertEntry(F, tracked, "src/a/b/.gitignore");
146 		assertEntry(F, tracked, "src/a/b/keep.o");
147 		assertEntry(F, ignored, "src/a/b/nothere.o");
148 
149 		assertEntry(D, tracked, "src/c");
150 		assertEntry(F, tracked, "src/c/.gitignore");
151 		assertEntry(D, tracked, "src/c/d");
152 		assertEntry(F, tracked, "src/c/d/.gitignore");
153 		assertEntry(F, tracked, "src/c/d/keep.o");
154 		// must be ignored: "!d" should not negate *both* "d" and *.o rules!
155 		assertEntry(F, ignored, "src/c/d/nothere.o");
156 		endWalk();
157 	}
158 
159 	/*
160 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=407475
161 	 */
162 	@Test
163 	public void testNegateAllExceptJavaInSrc() throws IOException {
164 		// ignore all files except from src directory
165 		writeIgnoreFile(".gitignore", "/*", "!/src/");
166 		writeTrashFile("nothere.o", "");
167 
168 		// ignore all files except java
169 		writeIgnoreFile("src/.gitignore", "*", "!*.java");
170 
171 		writeTrashFile("src/keep.java", "");
172 		writeTrashFile("src/nothere.o", "");
173 		writeTrashFile("src/a/nothere.o", "");
174 
175 		beginWalk();
176 		assertEntry(F, ignored, ".gitignore");
177 		assertEntry(F, ignored, "nothere.o");
178 		assertEntry(D, tracked, "src");
179 		assertEntry(F, ignored, "src/.gitignore");
180 		assertEntry(D, ignored, "src/a");
181 		assertEntry(F, ignored, "src/a/nothere.o");
182 		assertEntry(F, tracked, "src/keep.java");
183 		assertEntry(F, ignored, "src/nothere.o");
184 		endWalk();
185 	}
186 
187 	/*
188 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=407475
189 	 */
190 	@Test
191 	public void testNegationAllExceptJavaInSrcAndExceptChildDirInSrc()
192 			throws IOException {
193 		// ignore all files except from src directory
194 		writeIgnoreFile(".gitignore", "/*", "!/src/");
195 		writeTrashFile("nothere.o", "");
196 
197 		// ignore all files except java in src folder and all children folders.
198 		// Last ignore rule breaks old jgit via bug 407475
199 		writeIgnoreFile("src/.gitignore", "*", "!*.java", "!*/");
200 
201 		writeTrashFile("src/keep.java", "");
202 		writeTrashFile("src/nothere.o", "");
203 		writeTrashFile("src/a/keep.java", "");
204 		writeTrashFile("src/a/keep.o", "");
205 
206 		beginWalk();
207 		assertEntry(F, ignored, ".gitignore");
208 		assertEntry(F, ignored, "nothere.o");
209 		assertEntry(D, tracked, "src");
210 		assertEntry(F, ignored, "src/.gitignore");
211 		assertEntry(D, tracked, "src/a");
212 		assertEntry(F, tracked, "src/a/keep.java");
213 		assertEntry(F, tracked, "src/a/keep.o");
214 		assertEntry(F, tracked, "src/keep.java");
215 		assertEntry(F, ignored, "src/nothere.o");
216 		endWalk();
217 	}
218 
219 	/*
220 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
221 	 */
222 	@Test
223 	public void testRepeatedNegation() throws IOException {
224 		writeIgnoreFile(".gitignore", "e", "!e", "e", "!e", "e");
225 
226 		writeTrashFile("e/nothere.o", "");
227 
228 		beginWalk();
229 		assertEntry(F, tracked, ".gitignore");
230 		assertEntry(D, ignored, "e");
231 		assertEntry(F, ignored, "e/nothere.o");
232 		endWalk();
233 	}
234 
235 	/*
236 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
237 	 */
238 	@Test
239 	public void testRepeatedNegationInDifferentFiles1() throws IOException {
240 		writeIgnoreFile(".gitignore", "*.o", "e");
241 
242 		writeIgnoreFile("e/.gitignore", "!e");
243 		writeTrashFile("e/nothere.o", "");
244 
245 		beginWalk();
246 		assertEntry(F, tracked, ".gitignore");
247 		assertEntry(D, ignored, "e");
248 		assertEntry(F, ignored, "e/.gitignore");
249 		assertEntry(F, ignored, "e/nothere.o");
250 		endWalk();
251 	}
252 
253 	/*
254 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
255 	 */
256 	@Test
257 	public void testRepeatedNegationInDifferentFiles2() throws IOException {
258 		writeIgnoreFile(".gitignore", "*.o", "e");
259 
260 		writeIgnoreFile("a/.gitignore", "!e");
261 		writeTrashFile("a/e/nothere.o", "");
262 
263 		beginWalk();
264 		assertEntry(F, tracked, ".gitignore");
265 		assertEntry(D, tracked, "a");
266 		assertEntry(F, tracked, "a/.gitignore");
267 		assertEntry(D, tracked, "a/e");
268 		assertEntry(F, ignored, "a/e/nothere.o");
269 		endWalk();
270 	}
271 
272 	/*
273 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
274 	 */
275 	@Test
276 	public void testRepeatedNegationInDifferentFiles3() throws IOException {
277 		writeIgnoreFile(".gitignore", "*.o");
278 
279 		writeIgnoreFile("a/.gitignore", "e");
280 		writeIgnoreFile("a/b/.gitignore", "!e");
281 		writeTrashFile("a/b/e/nothere.o", "");
282 
283 		beginWalk();
284 		assertEntry(F, tracked, ".gitignore");
285 		assertEntry(D, tracked, "a");
286 		assertEntry(F, tracked, "a/.gitignore");
287 		assertEntry(D, tracked, "a/b");
288 		assertEntry(F, tracked, "a/b/.gitignore");
289 		assertEntry(D, tracked, "a/b/e");
290 		assertEntry(F, ignored, "a/b/e/nothere.o");
291 		endWalk();
292 	}
293 
294 	@Test
295 	public void testRepeatedNegationInDifferentFiles4() throws IOException {
296 		writeIgnoreFile(".gitignore", "*.o");
297 
298 		writeIgnoreFile("a/.gitignore", "e");
299 		// Rules are never empty: WorkingTreeIterator optimizes empty rules away
300 		// paranoia check in case this optimization will be removed
301 		writeIgnoreFile("a/b/.gitignore", "#");
302 		writeIgnoreFile("a/b/c/.gitignore", "!e");
303 		writeTrashFile("a/b/c/e/nothere.o", "");
304 
305 		beginWalk();
306 		assertEntry(F, tracked, ".gitignore");
307 		assertEntry(D, tracked, "a");
308 		assertEntry(F, tracked, "a/.gitignore");
309 		assertEntry(D, tracked, "a/b");
310 		assertEntry(F, tracked, "a/b/.gitignore");
311 		assertEntry(D, tracked, "a/b/c");
312 		assertEntry(F, tracked, "a/b/c/.gitignore");
313 		assertEntry(D, tracked, "a/b/c/e");
314 		assertEntry(F, ignored, "a/b/c/e/nothere.o");
315 		endWalk();
316 	}
317 
318 	@Test
319 	public void testEmptyIgnoreNode() {
320 		// Rules are never empty: WorkingTreeIterator optimizes empty files away
321 		// So we have to test it manually in case third party clients use
322 		// IgnoreNode directly.
323 		IgnoreNode node = new IgnoreNode();
324 		assertEquals(MatchResult.CHECK_PARENT, node.isIgnored("", false));
325 		assertEquals(MatchResult.CHECK_PARENT, node.isIgnored("", false, false));
326 		assertEquals(MatchResult.CHECK_PARENT_NEGATE_FIRST_MATCH,
327 				node.isIgnored("", false, true));
328 	}
329 
330 	@Test
331 	public void testEmptyIgnoreRules() throws IOException {
332 		IgnoreNode node = new IgnoreNode();
333 		node.parse(writeToString("", "#", "!", "[[=a=]]"));
334 		assertEquals(new ArrayList<>(), node.getRules());
335 		node.parse(writeToString(" ", " / "));
336 		assertEquals(2, node.getRules().size());
337 	}
338 
339 	@Test
340 	public void testSlashOnlyMatchesDirectory() throws IOException {
341 		writeIgnoreFile(".gitignore", "out/");
342 		writeTrashFile("out", "");
343 
344 		beginWalk();
345 		assertEntry(F, tracked, ".gitignore");
346 		assertEntry(F, tracked, "out");
347 
348 		FileUtils.delete(new File(trash, "out"));
349 		writeTrashFile("out/foo", "");
350 
351 		beginWalk();
352 		assertEntry(F, tracked, ".gitignore");
353 		assertEntry(D, ignored, "out");
354 		assertEntry(F, ignored, "out/foo");
355 		endWalk();
356 	}
357 
358 	@Test
359 	public void testSlashMatchesDirectory() throws IOException {
360 		writeIgnoreFile(".gitignore", "out2/");
361 
362 		writeTrashFile("out1/out1", "");
363 		writeTrashFile("out1/out2", "");
364 		writeTrashFile("out2/out1", "");
365 		writeTrashFile("out2/out2", "");
366 
367 		beginWalk();
368 		assertEntry(F, tracked, ".gitignore");
369 		assertEntry(D, tracked, "out1");
370 		assertEntry(F, tracked, "out1/out1");
371 		assertEntry(F, tracked, "out1/out2");
372 		assertEntry(D, ignored, "out2");
373 		assertEntry(F, ignored, "out2/out1");
374 		assertEntry(F, ignored, "out2/out2");
375 		endWalk();
376 	}
377 
378 	@Test
379 	public void testWildcardWithSlashMatchesDirectory() throws IOException {
380 		writeIgnoreFile(".gitignore", "out2*/");
381 
382 		writeTrashFile("out1/out1.txt", "");
383 		writeTrashFile("out1/out2", "");
384 		writeTrashFile("out1/out2.txt", "");
385 		writeTrashFile("out1/out2x/a", "");
386 		writeTrashFile("out2/out1.txt", "");
387 		writeTrashFile("out2/out2.txt", "");
388 		writeTrashFile("out2x/out1.txt", "");
389 		writeTrashFile("out2x/out2.txt", "");
390 
391 		beginWalk();
392 		assertEntry(F, tracked, ".gitignore");
393 		assertEntry(D, tracked, "out1");
394 		assertEntry(F, tracked, "out1/out1.txt");
395 		assertEntry(F, tracked, "out1/out2");
396 		assertEntry(F, tracked, "out1/out2.txt");
397 		assertEntry(D, ignored, "out1/out2x");
398 		assertEntry(F, ignored, "out1/out2x/a");
399 		assertEntry(D, ignored, "out2");
400 		assertEntry(F, ignored, "out2/out1.txt");
401 		assertEntry(F, ignored, "out2/out2.txt");
402 		assertEntry(D, ignored, "out2x");
403 		assertEntry(F, ignored, "out2x/out1.txt");
404 		assertEntry(F, ignored, "out2x/out2.txt");
405 		endWalk();
406 	}
407 
408 	@Test
409 	public void testWithSlashDoesNotMatchInSubDirectory() throws IOException {
410 		writeIgnoreFile(".gitignore", "a/b");
411 		writeTrashFile("a/a", "");
412 		writeTrashFile("a/b", "");
413 		writeTrashFile("src/a/a", "");
414 		writeTrashFile("src/a/b", "");
415 
416 		beginWalk();
417 		assertEntry(F, tracked, ".gitignore");
418 		assertEntry(D, tracked, "a");
419 		assertEntry(F, tracked, "a/a");
420 		assertEntry(F, ignored, "a/b");
421 		assertEntry(D, tracked, "src");
422 		assertEntry(D, tracked, "src/a");
423 		assertEntry(F, tracked, "src/a/a");
424 		assertEntry(F, tracked, "src/a/b");
425 		endWalk();
426 	}
427 
428 	@Test
429 	public void testNoPatterns() throws IOException {
430 		writeIgnoreFile(".gitignore", "", " ", "# comment", "/");
431 		writeTrashFile("a/a", "");
432 
433 		beginWalk();
434 		assertEntry(F, tracked, ".gitignore");
435 		assertEntry(D, tracked, "a");
436 		assertEntry(F, tracked, "a/a");
437 		endWalk();
438 	}
439 
440 	@Test
441 	public void testLeadingSpaces() throws IOException {
442 		writeTrashFile("  a/  a", "");
443 		writeTrashFile("  a/ a", "");
444 		writeTrashFile("  a/a", "");
445 		writeTrashFile(" a/  a", "");
446 		writeTrashFile(" a/ a", "");
447 		writeTrashFile(" a/a", "");
448 		writeIgnoreFile(".gitignore", " a", "  a");
449 		writeTrashFile("a/  a", "");
450 		writeTrashFile("a/ a", "");
451 		writeTrashFile("a/a", "");
452 
453 		beginWalk();
454 		assertEntry(D, ignored, "  a");
455 		assertEntry(F, ignored, "  a/  a");
456 		assertEntry(F, ignored, "  a/ a");
457 		assertEntry(F, ignored, "  a/a");
458 		assertEntry(D, ignored, " a");
459 		assertEntry(F, ignored, " a/  a");
460 		assertEntry(F, ignored, " a/ a");
461 		assertEntry(F, ignored, " a/a");
462 		assertEntry(F, tracked, ".gitignore");
463 		assertEntry(D, tracked, "a");
464 		assertEntry(F, ignored, "a/  a");
465 		assertEntry(F, ignored, "a/ a");
466 		assertEntry(F, tracked, "a/a");
467 		endWalk();
468 	}
469 
470 	@Test
471 	public void testTrailingSpaces() throws IOException {
472 		// Windows can't create files with trailing spaces
473 		// If this assumption fails the test is halted and ignored.
474 		org.junit.Assume.assumeFalse(SystemReader.getInstance().isWindows());
475 		writeTrashFile("a  /a", "");
476 		writeTrashFile("a  /a ", "");
477 		writeTrashFile("a  /a  ", "");
478 		writeTrashFile("a /a", "");
479 		writeTrashFile("a /a ", "");
480 		writeTrashFile("a /a  ", "");
481 		writeTrashFile("a/a", "");
482 		writeTrashFile("a/a ", "");
483 		writeTrashFile("a/a  ", "");
484 		writeTrashFile("b/c", "");
485 
486 		writeIgnoreFile(".gitignore", "a\\ ", "a \\ ", "b/ ");
487 
488 		beginWalk();
489 		assertEntry(F, tracked, ".gitignore");
490 		assertEntry(D, ignored, "a  ");
491 		assertEntry(F, ignored, "a  /a");
492 		assertEntry(F, ignored, "a  /a ");
493 		assertEntry(F, ignored, "a  /a  ");
494 		assertEntry(D, ignored, "a ");
495 		assertEntry(F, ignored, "a /a");
496 		assertEntry(F, ignored, "a /a ");
497 		assertEntry(F, ignored, "a /a  ");
498 		assertEntry(D, tracked, "a");
499 		assertEntry(F, tracked, "a/a");
500 		assertEntry(F, ignored, "a/a ");
501 		assertEntry(F, ignored, "a/a  ");
502 		assertEntry(D, ignored, "b");
503 		assertEntry(F, ignored, "b/c");
504 		endWalk();
505 	}
506 
507 	@Test
508 	public void testToString() throws Exception {
509 		assertEquals(Arrays.asList("").toString(), new IgnoreNode().toString());
510 		assertEquals(Arrays.asList("hello").toString(),
511 				new IgnoreNode(Arrays.asList(new FastIgnoreRule("hello")))
512 						.toString());
513 	}
514 
515 	private void beginWalk() {
516 		walk = new TreeWalk(db);
517 		walk.addTree(new FileTreeIterator(db));
518 	}
519 
520 	private void endWalk() throws IOException {
521 		assertFalse("Not all files tested", walk.next());
522 	}
523 
524 	private void assertEntry(FileMode type, boolean entryIgnored,
525 			String pathName) throws IOException {
526 		assertTrue("walk has entry", walk.next());
527 		assertEquals(pathName, walk.getPathString());
528 		assertEquals(type, walk.getFileMode(0));
529 
530 		WorkingTreeIterator itr = walk.getTree(0, WorkingTreeIterator.class);
531 		assertNotNull("has tree", itr);
532 		assertEquals("is ignored", entryIgnored, itr.isEntryIgnored());
533 		if (D.equals(type))
534 			walk.enterSubtree();
535 	}
536 
537 	private void writeIgnoreFile(String name, String... rules)
538 			throws IOException {
539 		StringBuilder data = new StringBuilder();
540 		for (String line : rules)
541 			data.append(line + "\n");
542 		writeTrashFile(name, data.toString());
543 	}
544 
545 	private InputStream writeToString(String... rules) {
546 		StringBuilder data = new StringBuilder();
547 		for (String line : rules) {
548 			data.append(line + "\n");
549 		}
550 		return new ByteArrayInputStream(data.toString().getBytes(UTF_8));
551 	}
552 }