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.attributes;
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  
16  import java.io.BufferedInputStream;
17  import java.io.BufferedReader;
18  import java.io.ByteArrayInputStream;
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStreamReader;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.eclipse.jgit.junit.RepositoryTestCase;
28  import org.eclipse.jgit.lib.StoredConfig;
29  import org.eclipse.jgit.treewalk.FileTreeIterator;
30  import org.eclipse.jgit.treewalk.TreeWalk;
31  import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
32  import org.eclipse.jgit.util.FS;
33  import org.eclipse.jgit.util.FS.ExecutionResult;
34  import org.eclipse.jgit.util.RawParseUtils;
35  import org.eclipse.jgit.util.TemporaryBuffer;
36  import org.junit.Before;
37  import org.junit.Test;
38  
39  /**
40   * Tests that verify that the attributes of files in a repository are the same
41   * in JGit and in C-git.
42   */
43  public class CGitAttributesTest extends RepositoryTestCase {
44  
45  	@Before
46  	public void initRepo() throws IOException {
47  		// Because we run C-git, we must ensure that global or user exclude
48  		// files cannot influence the tests. So we set core.excludesFile to an
49  		// empty file inside the repository.
50  		StoredConfig config = db.getConfig();
51  		File fakeUserGitignore = writeTrashFile(".fake_user_gitignore", "");
52  		config.setString("core", null, "excludesFile",
53  				fakeUserGitignore.getAbsolutePath());
54  		// Disable case-insensitivity -- JGit doesn't handle that yet.
55  		config.setBoolean("core", null, "ignoreCase", false);
56  		// And try to switch off the global attributes file, too.
57  		config.setString("core", null, "attributesFile",
58  				fakeUserGitignore.getAbsolutePath());
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 Attribute fromString(String key, String value) {
73  		if ("set".equals(value)) {
74  			return new Attribute(key, Attribute.State.SET);
75  		}
76  		if ("unset".equals(value)) {
77  			return new Attribute(key, Attribute.State.UNSET);
78  		}
79  		if ("unspecified".equals(value)) {
80  			return new Attribute(key, Attribute.State.UNSPECIFIED);
81  		}
82  		return new Attribute(key, value);
83  	}
84  
85  	private LinkedHashMap<String, Attributes> cgitAttributes(
86  			Set<String> allFiles) throws Exception {
87  		FS fs = db.getFS();
88  		StringBuilder input = new StringBuilder();
89  		for (String filename : allFiles) {
90  			input.append(filename).append('\n');
91  		}
92  		ProcessBuilder builder = fs.runInShell("git",
93  				new String[] { "check-attr", "--stdin", "--all" });
94  		builder.directory(db.getWorkTree());
95  		builder.environment().put("HOME", fs.userHome().getAbsolutePath());
96  		ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
97  				input.toString().getBytes(UTF_8)));
98  		String errorOut = toString(result.getStderr());
99  		assertEquals("External git failed", "exit 0\n",
100 				"exit " + result.getRc() + '\n' + errorOut);
101 		LinkedHashMap<String, Attributes> map = new LinkedHashMap<>();
102 		try (BufferedReader r = new BufferedReader(new InputStreamReader(
103 				new BufferedInputStream(result.getStdout().openInputStream()),
104 				UTF_8))) {
105 			r.lines().forEach(line -> {
106 				// Parse the line and add to result map
107 				int start = 0;
108 				int i = line.indexOf(':');
109 				String path = line.substring(0, i).trim();
110 				start = i + 1;
111 				i = line.indexOf(':', start);
112 				String key = line.substring(start, i).trim();
113 				String value = line.substring(i + 1).trim();
114 				Attribute attr = fromString(key, value);
115 				Attributes attrs = map.get(path);
116 				if (attrs == null) {
117 					attrs = new Attributes(attr);
118 					map.put(path, attrs);
119 				} else {
120 					attrs.put(attr);
121 				}
122 			});
123 		}
124 		return map;
125 	}
126 
127 	private LinkedHashMap<String, Attributes> jgitAttributes()
128 			throws IOException {
129 		// Do a tree walk and return a list of all files and directories with
130 		// their attributes
131 		LinkedHashMap<String, Attributes> result = new LinkedHashMap<>();
132 		try (TreeWalk walk = new TreeWalk(db)) {
133 			walk.addTree(new FileTreeIterator(db));
134 			walk.setFilter(new NotIgnoredFilter(0));
135 			while (walk.next()) {
136 				String path = walk.getPathString();
137 				if (walk.isSubtree() && !path.endsWith("/")) {
138 					// git check-attr expects directory paths to end with a
139 					// slash
140 					path += '/';
141 				}
142 				Attributes attrs = walk.getAttributes();
143 				if (attrs != null && !attrs.isEmpty()) {
144 					result.put(path, attrs);
145 				} else {
146 					result.put(path, null);
147 				}
148 				if (walk.isSubtree()) {
149 					walk.enterSubtree();
150 				}
151 			}
152 		}
153 		return result;
154 	}
155 
156 	private void assertSameAsCGit() throws Exception {
157 		LinkedHashMap<String, Attributes> jgit = jgitAttributes();
158 		LinkedHashMap<String, Attributes> cgit = cgitAttributes(jgit.keySet());
159 		// remove all without attributes
160 		Iterator<Map.Entry<String, Attributes>> iterator = jgit.entrySet()
161 				.iterator();
162 		while (iterator.hasNext()) {
163 			Map.Entry<String, Attributes> entry = iterator.next();
164 			if (entry.getValue() == null) {
165 				iterator.remove();
166 			}
167 		}
168 		assertArrayEquals("JGit attributes differ from C git",
169 				cgit.entrySet().toArray(), jgit.entrySet().toArray());
170 	}
171 
172 	@Test
173 	public void testBug508568() throws Exception {
174 		createFiles("foo.xml/bar.jar", "sub/foo.xml/bar.jar");
175 		writeTrashFile(".gitattributes", "*.xml xml\n" + "*.jar jar\n");
176 		assertSameAsCGit();
177 	}
178 
179 	@Test
180 	public void testRelativePath() throws Exception {
181 		createFiles("sub/foo.txt");
182 		writeTrashFile("sub/.gitattributes", "sub/** sub\n" + "*.txt txt\n");
183 		assertSameAsCGit();
184 	}
185 
186 	@Test
187 	public void testRelativePaths() throws Exception {
188 		createFiles("sub/foo.txt", "sub/sub/bar", "foo/sub/a.txt",
189 				"foo/sub/bar/a.tmp");
190 		writeTrashFile(".gitattributes", "sub/** sub\n" + "*.txt txt\n");
191 		assertSameAsCGit();
192 	}
193 
194 	@Test
195 	public void testNestedMatchNot() throws Exception {
196 		createFiles("foo.xml/bar.jar", "foo.xml/bar.xml", "sub/b.jar",
197 				"sub/b.xml");
198 		writeTrashFile("sub/.gitattributes", "*.xml xml\n" + "*.jar jar\n");
199 		assertSameAsCGit();
200 	}
201 
202 	@Test
203 	public void testNestedMatch() throws Exception {
204 		// This is an interesting test. At the time of this writing, the
205 		// gitignore documentation says: "In other words, foo/ will match a
206 		// directory foo AND PATHS UNDERNEATH IT, but will not match a regular
207 		// file or a symbolic link foo". (Emphasis added.) And gitattributes is
208 		// supposed to follow the same rules. But the documentation appears to
209 		// lie: C-git will *not* apply the attribute "xml" to *any* files in
210 		// any subfolder "foo" here. It will only apply the "jar" attribute
211 		// to the three *.jar files.
212 		//
213 		// The point is probably that ignores are handled top-down, and once a
214 		// directory "foo" is matched (here: on paths "foo" and "sub/foo" by
215 		// pattern "foo/"), the directory is excluded and the gitignore
216 		// documentation also says: "It is not possible to re-include a file if
217 		// a parent directory of that file is excluded." So once the pattern
218 		// "foo/" has matched, it appears as if everything beneath would also be
219 		// matched.
220 		//
221 		// But not so for gitattributes! The foo/ rule only matches the
222 		// directory itself, but not anything beneath.
223 		createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml",
224 				"sub/foo/b.jar");
225 		writeTrashFile(".gitattributes",
226 				"foo/ xml\n" + "sub/foo/ sub\n" + "*.jar jar\n");
227 		assertSameAsCGit();
228 	}
229 
230 	@Test
231 	public void testNestedMatchWithWildcard() throws Exception {
232 		// See above.
233 		createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml",
234 				"sub/foo/b.jar");
235 		writeTrashFile(".gitattributes",
236 				"**/foo/ xml\n" + "*/foo/ sub\n" + "*.jar jar\n");
237 		assertSameAsCGit();
238 	}
239 
240 	@Test
241 	public void testNestedMatchRecursive() throws Exception {
242 		createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml",
243 				"sub/foo/b.jar");
244 		writeTrashFile(".gitattributes", "foo/** xml\n" + "*.jar jar\n");
245 		assertSameAsCGit();
246 	}
247 
248 	@Test
249 	public void testStarMatchOnSlashNot() throws Exception {
250 		createFiles("sub/a.txt", "foo/sext", "foo/s.txt");
251 		writeTrashFile(".gitattributes", "s*xt bar");
252 		assertSameAsCGit();
253 	}
254 
255 	@Test
256 	public void testPrefixMatchNot() throws Exception {
257 		createFiles("src/new/foo.txt");
258 		writeTrashFile(".gitattributes", "src/new bar\n");
259 		assertSameAsCGit();
260 	}
261 
262 	@Test
263 	public void testComplexPathMatchNot() throws Exception {
264 		createFiles("src/new/foo.txt", "src/ndw");
265 		writeTrashFile(".gitattributes", "s[p-s]c/n[de]w bar\n");
266 		assertSameAsCGit();
267 	}
268 
269 	@Test
270 	public void testStarPathMatchNot() throws Exception {
271 		createFiles("src/new/foo.txt", "src/ndw");
272 		writeTrashFile(".gitattributes", "src/* bar\n");
273 		assertSameAsCGit();
274 	}
275 
276 	@Test
277 	public void testDirectoryMatchSubSimple() throws Exception {
278 		createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
279 		writeTrashFile(".gitattributes", "src/new/ bar\n");
280 		assertSameAsCGit();
281 	}
282 
283 	@Test
284 	public void testDirectoryMatchSubRecursive() throws Exception {
285 		createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
286 		writeTrashFile(".gitattributes", "**/src/new/ bar\n");
287 		assertSameAsCGit();
288 	}
289 
290 	@Test
291 	public void testDirectoryMatchSubRecursiveBacktrack() throws Exception {
292 		createFiles("src/new/foo.txt", "src/src/new/foo.txt");
293 		writeTrashFile(".gitattributes", "**/src/new/ bar\n");
294 		assertSameAsCGit();
295 	}
296 
297 	@Test
298 	public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception {
299 		createFiles("src/new/foo.txt", "src/src/new/foo.txt");
300 		writeTrashFile(".gitattributes", "**/**/src/new/ bar\n");
301 		assertSameAsCGit();
302 	}
303 
304 	@Test
305 	public void testDirectoryMatchSubRecursiveBacktrack3() throws Exception {
306 		createFiles("src/new/src/new/foo.txt",
307 				"foo/src/new/bar/src/new/foo.txt");
308 		writeTrashFile(".gitattributes", "**/src/new/ bar\n");
309 		assertSameAsCGit();
310 	}
311 
312 	@Test
313 	public void testDirectoryMatchSubRecursiveBacktrack4() throws Exception {
314 		createFiles("src/src/src/new/foo.txt",
315 				"foo/src/src/bar/src/new/foo.txt");
316 		writeTrashFile(".gitattributes", "**/src/ bar\n");
317 		assertSameAsCGit();
318 	}
319 
320 	@Test
321 	public void testDirectoryMatchSubRecursiveBacktrack5() throws Exception {
322 		createFiles("x/a/a/b/foo.txt", "x/y/z/b/a/b/foo.txt",
323 				"x/y/a/a/a/a/b/foo.txt", "x/y/a/a/a/a/b/a/b/foo.txt");
324 		writeTrashFile(".gitattributes", "**/*/a/b bar\n");
325 		assertSameAsCGit();
326 	}
327 
328 	@Test
329 	public void testDirectoryMatchSubRecursiveBacktrack6() throws Exception {
330 		createFiles("x/a/a/b/foo.txt", "x/y/a/b/a/b/foo.txt");
331 		writeTrashFile(".gitattributes", "**/*/**/a/b bar\n");
332 		assertSameAsCGit();
333 	}
334 
335 	@Test
336 	public void testDirectoryWildmatchDoesNotMatchFiles1() throws Exception {
337 		createFiles("a", "dir/b", "dir/sub/c");
338 		writeTrashFile(".gitattributes", "**/ bar\n");
339 		assertSameAsCGit();
340 	}
341 
342 	@Test
343 	public void testDirectoryWildmatchDoesNotMatchFiles2() throws Exception {
344 		createFiles("a", "dir/b", "dir/sub/c");
345 		writeTrashFile(".gitattributes", "**/**/ bar\n");
346 		assertSameAsCGit();
347 	}
348 
349 	@Test
350 	public void testDirectoryWildmatchDoesNotMatchFiles3() throws Exception {
351 		createFiles("a", "x/b", "sub/x/c", "sub/x/d/e");
352 		writeTrashFile(".gitattributes", "x/**/ bar\n");
353 		assertSameAsCGit();
354 	}
355 
356 	@Test
357 	public void testDirectoryWildmatchDoesNotMatchFiles4() throws Exception {
358 		createFiles("a", "dir/x", "dir/sub1/x", "dir/sub2/x/y");
359 		writeTrashFile(".gitattributes", "x/**/ bar\n");
360 		assertSameAsCGit();
361 	}
362 
363 	@Test
364 	public void testDirectoryMatchSubComplex() throws Exception {
365 		createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
366 		writeTrashFile(".gitattributes", "s[rs]c/n*/ bar\n");
367 		assertSameAsCGit();
368 	}
369 
370 	@Test
371 	public void testDirectoryMatch() throws Exception {
372 		createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
373 		writeTrashFile(".gitattributes", "new/ bar\n");
374 		assertSameAsCGit();
375 	}
376 
377 	@Test
378 	public void testBracketsInGroup() throws Exception {
379 		createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]");
380 		writeTrashFile(".gitattributes", "[[]] bar1\n" + "[\\[]] bar2\n"
381 				+ "[[\\]] bar3\n" + "[\\[\\]] bar4\n");
382 		assertSameAsCGit();
383 	}
384 }