View Javadoc
1   /*
2    * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> 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  package org.eclipse.jgit.ignore;
11  
12  import static java.nio.charset.StandardCharsets.UTF_8;
13  import static org.junit.Assert.assertArrayEquals;
14  import static org.junit.Assert.assertEquals;
15  import static org.junit.Assert.assertFalse;
16  import static org.junit.Assert.assertTrue;
17  
18  import java.io.BufferedInputStream;
19  import java.io.BufferedReader;
20  import java.io.ByteArrayInputStream;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStreamReader;
24  import java.nio.file.Files;
25  import java.util.LinkedHashSet;
26  import java.util.Set;
27  
28  import org.eclipse.jgit.junit.RepositoryTestCase;
29  import org.eclipse.jgit.lib.StoredConfig;
30  import org.eclipse.jgit.treewalk.FileTreeIterator;
31  import org.eclipse.jgit.treewalk.TreeWalk;
32  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
33  import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
34  import org.eclipse.jgit.util.FS;
35  import org.eclipse.jgit.util.FS.ExecutionResult;
36  import org.eclipse.jgit.util.RawParseUtils;
37  import org.eclipse.jgit.util.TemporaryBuffer;
38  import org.junit.Before;
39  import org.junit.Test;
40  
41  /**
42   * Tests that verify that the set of ignore files in a repository is the same in
43   * JGit and in C-git.
44   */
45  public class CGitIgnoreTest extends RepositoryTestCase {
46  
47  	@Before
48  	public void initRepo() throws IOException {
49  		// These tests focus on .gitignore files inside the repository. Because
50  		// we run C-git, we must ensure that global or user exclude files cannot
51  		// influence the tests. So we set core.excludesFile to an empty file
52  		// inside the repository.
53  		File fakeUserGitignore = writeTrashFile(".fake_user_gitignore", "");
54  		StoredConfig config = db.getConfig();
55  		config.setString("core", null, "excludesFile",
56  				fakeUserGitignore.getAbsolutePath());
57  		// Disable case-insensitivity -- JGit doesn't handle that yet.
58  		config.setBoolean("core", null, "ignoreCase", false);
59  		config.save();
60  	}
61  
62  	private void createFiles(String... paths) throws IOException {
63  		for (String path : paths) {
64  			writeTrashFile(path, "x");
65  		}
66  	}
67  
68  	private String toString(TemporaryBuffer b) throws IOException {
69  		return RawParseUtils.decode(b.toByteArray());
70  	}
71  
72  	private String[] cgitIgnored() throws Exception {
73  		FS fs = db.getFS();
74  		ProcessBuilder builder = fs.runInShell("git", new String[] { "ls-files",
75  				"--ignored", "--exclude-standard", "-o" });
76  		builder.directory(db.getWorkTree());
77  		builder.environment().put("HOME", fs.userHome().getAbsolutePath());
78  		ExecutionResult result = fs.execute(builder,
79  				new ByteArrayInputStream(new byte[0]));
80  		String errorOut = toString(result.getStderr());
81  		assertEquals("External git failed", "exit 0\n",
82  				"exit " + result.getRc() + '\n' + errorOut);
83  		try (BufferedReader r = new BufferedReader(new InputStreamReader(
84  				new BufferedInputStream(result.getStdout().openInputStream()),
85  				UTF_8))) {
86  			return r.lines().toArray(String[]::new);
87  		}
88  	}
89  
90  	private String[] cgitUntracked() throws Exception {
91  		FS fs = db.getFS();
92  		ProcessBuilder builder = fs.runInShell("git",
93  				new String[] { "ls-files", "--exclude-standard", "-o" });
94  		builder.directory(db.getWorkTree());
95  		builder.environment().put("HOME", fs.userHome().getAbsolutePath());
96  		ExecutionResult result = fs.execute(builder,
97  				new ByteArrayInputStream(new byte[0]));
98  		String errorOut = toString(result.getStderr());
99  		assertEquals("External git failed", "exit 0\n",
100 				"exit " + result.getRc() + '\n' + errorOut);
101 		try (BufferedReader r = new BufferedReader(new InputStreamReader(
102 				new BufferedInputStream(result.getStdout().openInputStream()),
103 				UTF_8))) {
104 			return r.lines().toArray(String[]::new);
105 		}
106 	}
107 
108 	private void jgitIgnoredAndUntracked(LinkedHashSet<String> ignored,
109 			LinkedHashSet<String> untracked) throws IOException {
110 		// Do a tree walk that does descend into ignored directories and return
111 		// a list of all ignored files
112 		try (TreeWalk walk = new TreeWalk(db)) {
113 			FileTreeIterator iter = new FileTreeIterator(db);
114 			iter.setWalkIgnoredDirectories(true);
115 			walk.addTree(iter);
116 			walk.setRecursive(true);
117 			while (walk.next()) {
118 				if (walk.getTree(WorkingTreeIterator.class).isEntryIgnored()) {
119 					ignored.add(walk.getPathString());
120 				} else {
121 					// tests of this class won't add any files to the index,
122 					// hence everything what is not ignored is untracked
123 					untracked.add(walk.getPathString());
124 				}
125 			}
126 		}
127 	}
128 
129 	private void assertNoIgnoredVisited(Set<String> ignored) throws Exception {
130 		// Do a recursive tree walk with a NotIgnoredFilter and verify that none
131 		// of the files visited is in the ignored set
132 		try (TreeWalk walk = new TreeWalk(db)) {
133 			walk.addTree(new FileTreeIterator(db));
134 			walk.setFilter(new NotIgnoredFilter(0));
135 			walk.setRecursive(true);
136 			while (walk.next()) {
137 				String path = walk.getPathString();
138 				assertFalse("File " + path + " is ignored, should not appear",
139 						ignored.contains(path));
140 			}
141 		}
142 	}
143 
144 	private void assertSameAsCGit(String... notIgnored) throws Exception {
145 		LinkedHashSet<String> ignored = new LinkedHashSet<>();
146 		LinkedHashSet<String> untracked = new LinkedHashSet<>();
147 		jgitIgnoredAndUntracked(ignored, untracked);
148 		String[] cgit = cgitIgnored();
149 		String[] cgitUntracked = cgitUntracked();
150 		assertArrayEquals(cgit, ignored.toArray());
151 		assertArrayEquals(cgitUntracked, untracked.toArray());
152 		for (String notExcluded : notIgnored) {
153 			assertFalse("File " + notExcluded + " should not be ignored",
154 					ignored.contains(notExcluded));
155 		}
156 		assertNoIgnoredVisited(ignored);
157 	}
158 
159 	@Test
160 	public void testSimpleIgnored() throws Exception {
161 		createFiles("a.txt", "a.tmp", "src/sub/a.txt", "src/a.tmp",
162 				"src/a.txt/b.tmp", "ignored/a.tmp", "ignored/not_ignored/a.tmp",
163 				"ignored/other/a.tmp");
164 		writeTrashFile(".gitignore",
165 				"*.txt\n" + "/ignored/*\n" + "!/ignored/not_ignored");
166 		assertSameAsCGit("ignored/not_ignored/a.tmp");
167 	}
168 
169 	@Test
170 	public void testDirOnlyMatch() throws Exception {
171 		createFiles("a.txt", "src/foo/a.txt", "src/a.txt", "foo/a.txt");
172 		writeTrashFile(".gitignore", "foo/");
173 		assertSameAsCGit();
174 	}
175 
176 	@Test
177 	public void testDirOnlyMatchDeep() throws Exception {
178 		createFiles("a.txt", "src/foo/a.txt", "src/a.txt", "foo/a.txt");
179 		writeTrashFile(".gitignore", "**/foo/");
180 		assertSameAsCGit();
181 	}
182 
183 	@Test
184 	public void testStarMatchOnSlashNot() throws Exception {
185 		createFiles("sub/a.txt", "foo/sext", "foo/s.txt");
186 		writeTrashFile(".gitignore", "s*xt");
187 		assertSameAsCGit("sub/a.txt");
188 	}
189 
190 	@Test
191 	public void testPrefixMatch() throws Exception {
192 		createFiles("src/new/foo.txt");
193 		writeTrashFile(".gitignore", "src/new");
194 		assertSameAsCGit();
195 	}
196 
197 	@Test
198 	public void testDirectoryMatchSubRecursive() throws Exception {
199 		createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
200 		writeTrashFile(".gitignore", "**/src/new/");
201 		assertSameAsCGit();
202 	}
203 
204 	@Test
205 	public void testDirectoryMatchSubRecursiveBacktrack() throws Exception {
206 		createFiles("src/new/foo.txt", "src/src/new/foo.txt");
207 		writeTrashFile(".gitignore", "**/src/new/");
208 		assertSameAsCGit();
209 	}
210 
211 	@Test
212 	public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception {
213 		createFiles("src/new/foo.txt", "src/src/new/foo.txt");
214 		writeTrashFile(".gitignore", "**/**/src/new/");
215 		assertSameAsCGit();
216 	}
217 
218 	@Test
219 	public void testDirectoryMatchSubRecursiveBacktrack3() throws Exception {
220 		createFiles("x/a/a/b/foo.txt");
221 		writeTrashFile(".gitignore", "**/*/a/b/");
222 		assertSameAsCGit();
223 	}
224 
225 	@Test
226 	public void testDirectoryMatchSubRecursiveBacktrack4() throws Exception {
227 		createFiles("x/a/a/b/foo.txt", "x/y/z/b/a/b/foo.txt",
228 				"x/y/a/a/a/a/b/foo.txt", "x/y/a/a/a/a/b/a/b/foo.txt");
229 		writeTrashFile(".gitignore", "**/*/a/b bar\n");
230 		assertSameAsCGit();
231 	}
232 
233 	@Test
234 	public void testDirectoryMatchSubRecursiveBacktrack5() throws Exception {
235 		createFiles("x/a/a/b/foo.txt", "x/y/a/b/a/b/foo.txt");
236 		writeTrashFile(".gitignore", "**/*/**/a/b bar\n");
237 		assertSameAsCGit();
238 	}
239 
240 	@Test
241 	public void testDirectoryWildmatchDoesNotMatchFiles1() throws Exception {
242 		createFiles("a", "dir/b", "dir/sub/c");
243 		writeTrashFile(".gitignore", "**/\n");
244 		assertSameAsCGit();
245 	}
246 
247 	@Test
248 	public void testDirectoryWildmatchDoesNotMatchFiles2() throws Exception {
249 		createFiles("a", "dir/b", "dir/sub/c");
250 		writeTrashFile(".gitignore", "**/**/\n");
251 		assertSameAsCGit();
252 	}
253 
254 	@Test
255 	public void testDirectoryWildmatchDoesNotMatchFiles3() throws Exception {
256 		createFiles("a", "x/b", "sub/x/c", "sub/x/d/e");
257 		writeTrashFile(".gitignore", "x/**/\n");
258 		assertSameAsCGit();
259 	}
260 
261 	@Test
262 	public void testDirectoryWildmatchDoesNotMatchFiles4() throws Exception {
263 		createFiles("a", "dir/x", "dir/sub1/x", "dir/sub2/x/y");
264 		writeTrashFile(".gitignore", "**/x/\n");
265 		assertSameAsCGit();
266 	}
267 
268 	@Test
269 	public void testUnescapedBracketsInGroup() throws Exception {
270 		createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]");
271 		writeTrashFile(".gitignore", "[[]]\n");
272 		assertSameAsCGit();
273 	}
274 
275 	@Test
276 	public void testEscapedFirstBracketInGroup() throws Exception {
277 		createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]");
278 		writeTrashFile(".gitignore", "[\\[]]\n");
279 		assertSameAsCGit();
280 	}
281 
282 	@Test
283 	public void testEscapedSecondBracketInGroup() throws Exception {
284 		createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]");
285 		writeTrashFile(".gitignore", "[[\\]]\n");
286 		assertSameAsCGit();
287 	}
288 
289 	@Test
290 	public void testEscapedBothBracketsInGroup() throws Exception {
291 		createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]");
292 		writeTrashFile(".gitignore", "[\\[\\]]\n");
293 		assertSameAsCGit();
294 	}
295 
296 	@Test
297 	public void testSimpleRootGitIgnoreGlobalNegation1() throws Exception {
298 		// see IgnoreNodeTest.testSimpleRootGitIgnoreGlobalNegation1
299 		createFiles("x1", "a/x2", "x3/y");
300 		writeTrashFile(".gitignore", "*\n!x*");
301 		assertSameAsCGit();
302 	}
303 
304 	@Test
305 	public void testRepeatedNegationInDifferentFiles5() throws Exception {
306 		// see IgnoreNodeTest.testRepeatedNegationInDifferentFiles5
307 		createFiles("a/b/e/nothere.o");
308 		writeTrashFile(".gitignore", "e");
309 		writeTrashFile("a/.gitignore", "e");
310 		writeTrashFile("a/b/.gitignore", "!e");
311 		assertSameAsCGit();
312 	}
313 
314 	@Test
315 	public void testRepeatedNegationInDifferentFilesWithWildmatcher1()
316 			throws Exception {
317 		createFiles("e", "x/e/f", "a/e/x1", "a/e/x2", "a/e/y", "a/e/sub/y");
318 		writeTrashFile(".gitignore", "a/e/**");
319 		writeTrashFile("a/.gitignore", "!e/x*");
320 		assertSameAsCGit();
321 	}
322 
323 	@Test
324 	public void testRepeatedNegationInDifferentFilesWithWildmatcher2()
325 			throws Exception {
326 		createFiles("e", "dir/f", "dir/g/h", "a/dir/i", "a/dir/j/k",
327 				"a/b/dir/l", "a/b/dir/m/n", "a/b/dir/m/o/p", "a/q/dir/r",
328 				"a/q/dir/dir/s", "c/d/dir/x", "c/d/dir/y");
329 		writeTrashFile(".gitignore", "**/dir/*");
330 		writeTrashFile("a/.gitignore", "!dir/*");
331 		writeTrashFile("a/b/.gitignore", "!**/dir/*");
332 		writeTrashFile("c/.gitignore", "!d/dir/x");
333 		assertSameAsCGit();
334 	}
335 
336 	@Test
337 	public void testNegationForSubDirectoryWithinIgnoredDirectoryHasNoEffect1()
338 			throws Exception {
339 		createFiles("e", "a/f", "a/b/g", "a/b/h/i");
340 		writeTrashFile(".gitignore", "a/b");
341 		writeTrashFile("a/.gitignore", "!b/*");
342 		assertSameAsCGit();
343 	}
344 
345 	/*
346 	 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=407475
347 	 */
348 	@Test
349 	public void testNegationAllExceptJavaInSrcAndExceptChildDirInSrc()
350 			throws Exception {
351 		// see
352 		// IgnoreNodeTest.testNegationAllExceptJavaInSrcAndExceptChildDirInSrc
353 		createFiles("nothere.o", "src/keep.java", "src/nothere.o",
354 				"src/a/keep.java", "src/a/keep.o");
355 		writeTrashFile(".gitignore", "/*\n!/src/");
356 		writeTrashFile("src/.gitignore", "*\n!*.java\n!*/");
357 		assertSameAsCGit();
358 	}
359 
360 	@Test
361 	public void testMultipleEntriesIgnored() throws Exception {
362 		createFiles("dir/a");
363 		writeTrashFile(".gitignore", "!dir/a\ndir/a");
364 		assertSameAsCGit();
365 	}
366 
367 	@Test
368 	public void testMultipleEntriesNotIgnored() throws Exception {
369 		createFiles("dir/a");
370 		writeTrashFile(".gitignore", "dir/a\n!dir/a");
371 		assertSameAsCGit("dir/a");
372 	}
373 
374 	@Test
375 	public void testInfoExcludes() throws Exception {
376 		createFiles("dir/a", "dir/b");
377 		File gitDir = db.getDirectory();
378 		File info = new File(gitDir, "info");
379 		assertTrue(info.mkdirs());
380 		File infoExclude = new File(info, "exclude");
381 		Files.writeString(infoExclude.toPath(), "dir/a");
382 		assertSameAsCGit("dir/b");
383 	}
384 
385 	@Test
386 	public void testInfoExcludesPrecedence() throws Exception {
387 		createFiles("dir/a", "dir/b");
388 		writeTrashFile(".gitignore", "!dir/a");
389 		File gitDir = db.getDirectory();
390 		File info = new File(gitDir, "info");
391 		assertTrue(info.mkdirs());
392 		File infoExclude = new File(info, "exclude");
393 		Files.writeString(infoExclude.toPath(), "dir/a");
394 		assertSameAsCGit("dir/a", "dir/b");
395 	}
396 
397 	@Test
398 	public void testCoreExcludes() throws Exception {
399 		createFiles("dir/a", "dir/b");
400 		writeTrashFile(".fake_git_ignore", "dir/a");
401 		assertSameAsCGit("dir/b");
402 	}
403 
404 	@Test
405 	public void testInfoCoreExcludes() throws Exception {
406 		createFiles("dir/a", "dir/b");
407 		File gitDir = db.getDirectory();
408 		File info = new File(gitDir, "info");
409 		assertTrue(info.mkdirs());
410 		File infoExclude = new File(info, "exclude");
411 		Files.writeString(infoExclude.toPath(), "!a");
412 		writeTrashFile(".fake_git_ignore", "dir/a");
413 		assertSameAsCGit("dir/b");
414 	}
415 
416 	@Test
417 	public void testInfoCoreExcludesPrecedence() throws Exception {
418 		createFiles("dir/a", "dir/b");
419 		File gitDir = db.getDirectory();
420 		File info = new File(gitDir, "info");
421 		assertTrue(info.mkdirs());
422 		File infoExclude = new File(info, "exclude");
423 		Files.writeString(infoExclude.toPath(), "!dir/a");
424 		writeTrashFile(".fake_git_ignore", "dir/a");
425 		assertSameAsCGit("dir/a", "dir/b");
426 	}
427 
428 	@Test
429 	public void testInfoCoreExcludesPrecedence2() throws Exception {
430 		createFiles("dir/a", "dir/b");
431 		File gitDir = db.getDirectory();
432 		File info = new File(gitDir, "info");
433 		assertTrue(info.mkdirs());
434 		File infoExclude = new File(info, "exclude");
435 		Files.writeString(infoExclude.toPath(), "dir/a");
436 		writeTrashFile(".fake_git_ignore", "!dir/a");
437 		assertSameAsCGit("dir/b");
438 	}
439 }