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