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 org.eclipse.jgit.junit.Assert.assertEquals;
46  import static org.junit.Assert.assertEquals;
47  import static org.junit.Assert.assertFalse;
48  import static org.junit.Assert.assertNotNull;
49  import static org.junit.Assert.assertTrue;
50  
51  import java.io.ByteArrayInputStream;
52  import java.io.File;
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.util.ArrayList;
56  import java.util.Arrays;
57  
58  import org.eclipse.jgit.ignore.IgnoreNode.MatchResult;
59  import org.eclipse.jgit.junit.RepositoryTestCase;
60  import org.eclipse.jgit.lib.FileMode;
61  import org.eclipse.jgit.treewalk.FileTreeIterator;
62  import org.eclipse.jgit.treewalk.TreeWalk;
63  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
64  import org.eclipse.jgit.util.FileUtils;
65  import org.eclipse.jgit.util.SystemReader;
66  import org.junit.Test;
67  
68  /**
69   * Tests ignore node behavior on the local filesystem.
70   */
71  public class IgnoreNodeTest extends RepositoryTestCase {
72  	private static final FileMode D = FileMode.TREE;
73  
74  	private static final FileMode F = FileMode.REGULAR_FILE;
75  
76  	private static final boolean ignored = true;
77  
78  	private static final boolean tracked = false;
79  
80  	private TreeWalk walk;
81  
82  	@Test
83  	public void testRules() throws IOException {
84  		writeIgnoreFile(".git/info/exclude", "*~", "/out");
85  
86  		writeIgnoreFile(".gitignore", "*.o", "/config");
87  		writeTrashFile("config/secret", "");
88  		writeTrashFile("mylib.c", "");
89  		writeTrashFile("mylib.c~", "");
90  		writeTrashFile("mylib.o", "");
91  
92  		writeTrashFile("out/object/foo.exe", "");
93  		writeIgnoreFile("src/config/.gitignore", "lex.out");
94  		writeTrashFile("src/config/lex.out", "");
95  		writeTrashFile("src/config/config.c", "");
96  		writeTrashFile("src/config/config.c~", "");
97  		writeTrashFile("src/config/old/lex.out", "");
98  
99  		beginWalk();
100 		assertEntry(F, tracked, ".gitignore");
101 		assertEntry(D, ignored, "config");
102 		assertEntry(F, ignored, "config/secret");
103 		assertEntry(F, tracked, "mylib.c");
104 		assertEntry(F, ignored, "mylib.c~");
105 		assertEntry(F, ignored, "mylib.o");
106 
107 		assertEntry(D, ignored, "out");
108 		assertEntry(D, ignored, "out/object");
109 		assertEntry(F, ignored, "out/object/foo.exe");
110 
111 		assertEntry(D, tracked, "src");
112 		assertEntry(D, tracked, "src/config");
113 		assertEntry(F, tracked, "src/config/.gitignore");
114 		assertEntry(F, tracked, "src/config/config.c");
115 		assertEntry(F, ignored, "src/config/config.c~");
116 		assertEntry(F, ignored, "src/config/lex.out");
117 		assertEntry(D, tracked, "src/config/old");
118 		assertEntry(F, ignored, "src/config/old/lex.out");
119 		endWalk();
120 	}
121 
122 	@Test
123 	public void testNegation() throws IOException {
124 		// ignore all *.o files and ignore all "d" directories
125 		writeIgnoreFile(".gitignore", "*.o", "d");
126 
127 		// negate "ignore" for a/b/keep.o file only
128 		writeIgnoreFile("src/a/b/.gitignore", "!keep.o");
129 		writeTrashFile("src/a/b/keep.o", "");
130 		writeTrashFile("src/a/b/nothere.o", "");
131 
132 		// negate "ignore" for "d"
133 		writeIgnoreFile("src/c/.gitignore", "!d");
134 		// negate "ignore" for c/d/keep.o file only
135 		writeIgnoreFile("src/c/d/.gitignore", "!keep.o");
136 		writeTrashFile("src/c/d/keep.o", "");
137 		writeTrashFile("src/c/d/nothere.o", "");
138 
139 		beginWalk();
140 		assertEntry(F, tracked, ".gitignore");
141 		assertEntry(D, tracked, "src");
142 		assertEntry(D, tracked, "src/a");
143 		assertEntry(D, tracked, "src/a/b");
144 		assertEntry(F, tracked, "src/a/b/.gitignore");
145 		assertEntry(F, tracked, "src/a/b/keep.o");
146 		assertEntry(F, ignored, "src/a/b/nothere.o");
147 
148 		assertEntry(D, tracked, "src/c");
149 		assertEntry(F, tracked, "src/c/.gitignore");
150 		assertEntry(D, tracked, "src/c/d");
151 		assertEntry(F, tracked, "src/c/d/.gitignore");
152 		assertEntry(F, tracked, "src/c/d/keep.o");
153 		// must be ignored: "!d" should not negate *both* "d" and *.o rules!
154 		assertEntry(F, ignored, "src/c/d/nothere.o");
155 		endWalk();
156 	}
157 
158 	/*
159 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=407475
160 	 */
161 	@Test
162 	public void testNegateAllExceptJavaInSrc() throws IOException {
163 		// ignore all files except from src directory
164 		writeIgnoreFile(".gitignore", "/*", "!/src/");
165 		writeTrashFile("nothere.o", "");
166 
167 		// ignore all files except java
168 		writeIgnoreFile("src/.gitignore", "*", "!*.java");
169 
170 		writeTrashFile("src/keep.java", "");
171 		writeTrashFile("src/nothere.o", "");
172 		writeTrashFile("src/a/nothere.o", "");
173 
174 		beginWalk();
175 		assertEntry(F, ignored, ".gitignore");
176 		assertEntry(F, ignored, "nothere.o");
177 		assertEntry(D, tracked, "src");
178 		assertEntry(F, ignored, "src/.gitignore");
179 		assertEntry(D, ignored, "src/a");
180 		assertEntry(F, ignored, "src/a/nothere.o");
181 		assertEntry(F, tracked, "src/keep.java");
182 		assertEntry(F, ignored, "src/nothere.o");
183 		endWalk();
184 	}
185 
186 	/*
187 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=407475
188 	 */
189 	@Test
190 	public void testNegationAllExceptJavaInSrcAndExceptChildDirInSrc()
191 			throws IOException {
192 		// ignore all files except from src directory
193 		writeIgnoreFile(".gitignore", "/*", "!/src/");
194 		writeTrashFile("nothere.o", "");
195 
196 		// ignore all files except java in src folder and all children folders.
197 		// Last ignore rule breaks old jgit via bug 407475
198 		writeIgnoreFile("src/.gitignore", "*", "!*.java", "!*/");
199 
200 		writeTrashFile("src/keep.java", "");
201 		writeTrashFile("src/nothere.o", "");
202 		writeTrashFile("src/a/keep.java", "");
203 		writeTrashFile("src/a/keep.o", "");
204 
205 		beginWalk();
206 		assertEntry(F, ignored, ".gitignore");
207 		assertEntry(F, ignored, "nothere.o");
208 		assertEntry(D, tracked, "src");
209 		assertEntry(F, ignored, "src/.gitignore");
210 		assertEntry(D, tracked, "src/a");
211 		assertEntry(F, tracked, "src/a/keep.java");
212 		assertEntry(F, tracked, "src/a/keep.o");
213 		assertEntry(F, tracked, "src/keep.java");
214 		assertEntry(F, ignored, "src/nothere.o");
215 		endWalk();
216 	}
217 
218 	/*
219 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
220 	 */
221 	@Test
222 	public void testRepeatedNegation() throws IOException {
223 		writeIgnoreFile(".gitignore", "e", "!e", "e", "!e", "e");
224 
225 		writeTrashFile("e/nothere.o", "");
226 
227 		beginWalk();
228 		assertEntry(F, tracked, ".gitignore");
229 		assertEntry(D, ignored, "e");
230 		assertEntry(F, ignored, "e/nothere.o");
231 		endWalk();
232 	}
233 
234 	/*
235 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
236 	 */
237 	@Test
238 	public void testRepeatedNegationInDifferentFiles1() throws IOException {
239 		writeIgnoreFile(".gitignore", "*.o", "e");
240 
241 		writeIgnoreFile("e/.gitignore", "!e");
242 		writeTrashFile("e/nothere.o", "");
243 
244 		beginWalk();
245 		assertEntry(F, tracked, ".gitignore");
246 		assertEntry(D, ignored, "e");
247 		assertEntry(F, ignored, "e/.gitignore");
248 		assertEntry(F, ignored, "e/nothere.o");
249 		endWalk();
250 	}
251 
252 	/*
253 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
254 	 */
255 	@Test
256 	public void testRepeatedNegationInDifferentFiles2() throws IOException {
257 		writeIgnoreFile(".gitignore", "*.o", "e");
258 
259 		writeIgnoreFile("a/.gitignore", "!e");
260 		writeTrashFile("a/e/nothere.o", "");
261 
262 		beginWalk();
263 		assertEntry(F, tracked, ".gitignore");
264 		assertEntry(D, tracked, "a");
265 		assertEntry(F, tracked, "a/.gitignore");
266 		assertEntry(D, tracked, "a/e");
267 		assertEntry(F, ignored, "a/e/nothere.o");
268 		endWalk();
269 	}
270 
271 	/*
272 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=448094
273 	 */
274 	@Test
275 	public void testRepeatedNegationInDifferentFiles3() throws IOException {
276 		writeIgnoreFile(".gitignore", "*.o");
277 
278 		writeIgnoreFile("a/.gitignore", "e");
279 		writeIgnoreFile("a/b/.gitignore", "!e");
280 		writeTrashFile("a/b/e/nothere.o", "");
281 
282 		beginWalk();
283 		assertEntry(F, tracked, ".gitignore");
284 		assertEntry(D, tracked, "a");
285 		assertEntry(F, tracked, "a/.gitignore");
286 		assertEntry(D, tracked, "a/b");
287 		assertEntry(F, tracked, "a/b/.gitignore");
288 		assertEntry(D, tracked, "a/b/e");
289 		assertEntry(F, ignored, "a/b/e/nothere.o");
290 		endWalk();
291 	}
292 
293 	@Test
294 	public void testRepeatedNegationInDifferentFiles4() throws IOException {
295 		writeIgnoreFile(".gitignore", "*.o");
296 
297 		writeIgnoreFile("a/.gitignore", "e");
298 		// Rules are never empty: WorkingTreeIterator optimizes empty rules away
299 		// paranoia check in case this optimization will be removed
300 		writeIgnoreFile("a/b/.gitignore", "#");
301 		writeIgnoreFile("a/b/c/.gitignore", "!e");
302 		writeTrashFile("a/b/c/e/nothere.o", "");
303 
304 		beginWalk();
305 		assertEntry(F, tracked, ".gitignore");
306 		assertEntry(D, tracked, "a");
307 		assertEntry(F, tracked, "a/.gitignore");
308 		assertEntry(D, tracked, "a/b");
309 		assertEntry(F, tracked, "a/b/.gitignore");
310 		assertEntry(D, tracked, "a/b/c");
311 		assertEntry(F, tracked, "a/b/c/.gitignore");
312 		assertEntry(D, tracked, "a/b/c/e");
313 		assertEntry(F, ignored, "a/b/c/e/nothere.o");
314 		endWalk();
315 	}
316 
317 	@Test
318 	public void testEmptyIgnoreNode() {
319 		// Rules are never empty: WorkingTreeIterator optimizes empty files away
320 		// So we have to test it manually in case third party clients use
321 		// IgnoreNode directly.
322 		IgnoreNode node = new IgnoreNode();
323 		assertEquals(MatchResult.CHECK_PARENT, node.isIgnored("", false));
324 		assertEquals(MatchResult.CHECK_PARENT, node.isIgnored("", false, false));
325 		assertEquals(MatchResult.CHECK_PARENT_NEGATE_FIRST_MATCH,
326 				node.isIgnored("", false, true));
327 	}
328 
329 	@Test
330 	public void testEmptyIgnoreRules() throws IOException {
331 		IgnoreNode node = new IgnoreNode();
332 		node.parse(writeToString("", "#", "!", "[[=a=]]"));
333 		assertEquals(new ArrayList<>(), node.getRules());
334 		node.parse(writeToString(" ", " / "));
335 		assertEquals(2, node.getRules().size());
336 	}
337 
338 	@Test
339 	public void testSlashOnlyMatchesDirectory() throws IOException {
340 		writeIgnoreFile(".gitignore", "out/");
341 		writeTrashFile("out", "");
342 
343 		beginWalk();
344 		assertEntry(F, tracked, ".gitignore");
345 		assertEntry(F, tracked, "out");
346 
347 		FileUtils.delete(new File(trash, "out"));
348 		writeTrashFile("out/foo", "");
349 
350 		beginWalk();
351 		assertEntry(F, tracked, ".gitignore");
352 		assertEntry(D, ignored, "out");
353 		assertEntry(F, ignored, "out/foo");
354 		endWalk();
355 	}
356 
357 	@Test
358 	public void testSlashMatchesDirectory() throws IOException {
359 		writeIgnoreFile(".gitignore", "out2/");
360 
361 		writeTrashFile("out1/out1", "");
362 		writeTrashFile("out1/out2", "");
363 		writeTrashFile("out2/out1", "");
364 		writeTrashFile("out2/out2", "");
365 
366 		beginWalk();
367 		assertEntry(F, tracked, ".gitignore");
368 		assertEntry(D, tracked, "out1");
369 		assertEntry(F, tracked, "out1/out1");
370 		assertEntry(F, tracked, "out1/out2");
371 		assertEntry(D, ignored, "out2");
372 		assertEntry(F, ignored, "out2/out1");
373 		assertEntry(F, ignored, "out2/out2");
374 		endWalk();
375 	}
376 
377 	@Test
378 	public void testWildcardWithSlashMatchesDirectory() throws IOException {
379 		writeIgnoreFile(".gitignore", "out2*/");
380 
381 		writeTrashFile("out1/out1.txt", "");
382 		writeTrashFile("out1/out2", "");
383 		writeTrashFile("out1/out2.txt", "");
384 		writeTrashFile("out1/out2x/a", "");
385 		writeTrashFile("out2/out1.txt", "");
386 		writeTrashFile("out2/out2.txt", "");
387 		writeTrashFile("out2x/out1.txt", "");
388 		writeTrashFile("out2x/out2.txt", "");
389 
390 		beginWalk();
391 		assertEntry(F, tracked, ".gitignore");
392 		assertEntry(D, tracked, "out1");
393 		assertEntry(F, tracked, "out1/out1.txt");
394 		assertEntry(F, tracked, "out1/out2");
395 		assertEntry(F, tracked, "out1/out2.txt");
396 		assertEntry(D, ignored, "out1/out2x");
397 		assertEntry(F, ignored, "out1/out2x/a");
398 		assertEntry(D, ignored, "out2");
399 		assertEntry(F, ignored, "out2/out1.txt");
400 		assertEntry(F, ignored, "out2/out2.txt");
401 		assertEntry(D, ignored, "out2x");
402 		assertEntry(F, ignored, "out2x/out1.txt");
403 		assertEntry(F, ignored, "out2x/out2.txt");
404 		endWalk();
405 	}
406 
407 	@Test
408 	public void testWithSlashDoesNotMatchInSubDirectory() throws IOException {
409 		writeIgnoreFile(".gitignore", "a/b");
410 		writeTrashFile("a/a", "");
411 		writeTrashFile("a/b", "");
412 		writeTrashFile("src/a/a", "");
413 		writeTrashFile("src/a/b", "");
414 
415 		beginWalk();
416 		assertEntry(F, tracked, ".gitignore");
417 		assertEntry(D, tracked, "a");
418 		assertEntry(F, tracked, "a/a");
419 		assertEntry(F, ignored, "a/b");
420 		assertEntry(D, tracked, "src");
421 		assertEntry(D, tracked, "src/a");
422 		assertEntry(F, tracked, "src/a/a");
423 		assertEntry(F, tracked, "src/a/b");
424 		endWalk();
425 	}
426 
427 	@Test
428 	public void testNoPatterns() throws IOException {
429 		writeIgnoreFile(".gitignore", "", " ", "# comment", "/");
430 		writeTrashFile("a/a", "");
431 
432 		beginWalk();
433 		assertEntry(F, tracked, ".gitignore");
434 		assertEntry(D, tracked, "a");
435 		assertEntry(F, tracked, "a/a");
436 		endWalk();
437 	}
438 
439 	@Test
440 	public void testLeadingSpaces() throws IOException {
441 		writeTrashFile("  a/  a", "");
442 		writeTrashFile("  a/ a", "");
443 		writeTrashFile("  a/a", "");
444 		writeTrashFile(" a/  a", "");
445 		writeTrashFile(" a/ a", "");
446 		writeTrashFile(" a/a", "");
447 		writeIgnoreFile(".gitignore", " a", "  a");
448 		writeTrashFile("a/  a", "");
449 		writeTrashFile("a/ a", "");
450 		writeTrashFile("a/a", "");
451 
452 		beginWalk();
453 		assertEntry(D, ignored, "  a");
454 		assertEntry(F, ignored, "  a/  a");
455 		assertEntry(F, ignored, "  a/ a");
456 		assertEntry(F, ignored, "  a/a");
457 		assertEntry(D, ignored, " a");
458 		assertEntry(F, ignored, " a/  a");
459 		assertEntry(F, ignored, " a/ a");
460 		assertEntry(F, ignored, " a/a");
461 		assertEntry(F, tracked, ".gitignore");
462 		assertEntry(D, tracked, "a");
463 		assertEntry(F, ignored, "a/  a");
464 		assertEntry(F, ignored, "a/ a");
465 		assertEntry(F, tracked, "a/a");
466 		endWalk();
467 	}
468 
469 	@Test
470 	public void testTrailingSpaces() throws IOException {
471 		// Windows can't create files with trailing spaces
472 		// If this assumption fails the test is halted and ignored.
473 		org.junit.Assume.assumeFalse(SystemReader.getInstance().isWindows());
474 		writeTrashFile("a  /a", "");
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 
484 		writeIgnoreFile(".gitignore", "a\\ ", "a \\ ");
485 
486 		beginWalk();
487 		assertEntry(F, tracked, ".gitignore");
488 		assertEntry(D, ignored, "a  ");
489 		assertEntry(F, ignored, "a  /a");
490 		assertEntry(F, ignored, "a  /a ");
491 		assertEntry(F, ignored, "a  /a  ");
492 		assertEntry(D, ignored, "a ");
493 		assertEntry(F, ignored, "a /a");
494 		assertEntry(F, ignored, "a /a ");
495 		assertEntry(F, ignored, "a /a  ");
496 		assertEntry(D, tracked, "a");
497 		assertEntry(F, tracked, "a/a");
498 		assertEntry(F, ignored, "a/a ");
499 		assertEntry(F, ignored, "a/a  ");
500 		endWalk();
501 	}
502 
503 	@Test
504 	public void testToString() throws Exception {
505 		assertEquals(Arrays.asList("").toString(), new IgnoreNode().toString());
506 		assertEquals(Arrays.asList("hello").toString(),
507 				new IgnoreNode(Arrays.asList(new FastIgnoreRule("hello")))
508 						.toString());
509 	}
510 
511 	private void beginWalk() {
512 		walk = new TreeWalk(db);
513 		walk.addTree(new FileTreeIterator(db));
514 	}
515 
516 	private void endWalk() throws IOException {
517 		assertFalse("Not all files tested", walk.next());
518 	}
519 
520 	private void assertEntry(FileMode type, boolean entryIgnored,
521 			String pathName) throws IOException {
522 		assertTrue("walk has entry", walk.next());
523 		assertEquals(pathName, walk.getPathString());
524 		assertEquals(type, walk.getFileMode(0));
525 
526 		WorkingTreeIterator itr = walk.getTree(0, WorkingTreeIterator.class);
527 		assertNotNull("has tree", itr);
528 		assertEquals("is ignored", entryIgnored, itr.isEntryIgnored());
529 		if (D.equals(type))
530 			walk.enterSubtree();
531 	}
532 
533 	private void writeIgnoreFile(String name, String... rules)
534 			throws IOException {
535 		StringBuilder data = new StringBuilder();
536 		for (String line : rules)
537 			data.append(line + "\n");
538 		writeTrashFile(name, data.toString());
539 	}
540 
541 	private InputStream writeToString(String... rules) throws IOException {
542 		StringBuilder data = new StringBuilder();
543 		for (String line : rules) {
544 			data.append(line + "\n");
545 		}
546 		return new ByteArrayInputStream(data.toString().getBytes("UTF-8"));
547 	}
548 }