View Javadoc
1   /*
2    * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) 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.merge;
11  
12  import static org.junit.Assert.assertEquals;
13  import static org.junit.Assert.assertFalse;
14  import static org.junit.Assert.assertNotNull;
15  import static org.junit.Assert.assertNull;
16  import static org.junit.Assert.assertTrue;
17  
18  import java.io.BufferedInputStream;
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.nio.file.Files;
24  import java.util.function.Consumer;
25  
26  import org.eclipse.jgit.api.Git;
27  import org.eclipse.jgit.api.MergeResult;
28  import org.eclipse.jgit.api.MergeResult.MergeStatus;
29  import org.eclipse.jgit.api.errors.CheckoutConflictException;
30  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
31  import org.eclipse.jgit.api.errors.GitAPIException;
32  import org.eclipse.jgit.api.errors.InvalidMergeHeadsException;
33  import org.eclipse.jgit.api.errors.NoFilepatternException;
34  import org.eclipse.jgit.api.errors.NoHeadException;
35  import org.eclipse.jgit.api.errors.NoMessageException;
36  import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
37  import org.eclipse.jgit.attributes.Attribute;
38  import org.eclipse.jgit.attributes.Attributes;
39  import org.eclipse.jgit.errors.NoWorkTreeException;
40  import org.eclipse.jgit.junit.RepositoryTestCase;
41  import org.eclipse.jgit.revwalk.RevCommit;
42  import org.eclipse.jgit.treewalk.FileTreeIterator;
43  import org.eclipse.jgit.treewalk.TreeWalk;
44  import org.eclipse.jgit.treewalk.filter.PathFilter;
45  import org.junit.Ignore;
46  import org.junit.Test;
47  
48  public class MergeGitAttributeTest extends RepositoryTestCase {
49  
50  	private static final String REFS_HEADS_RIGHT = "refs/heads/right";
51  
52  	private static final String REFS_HEADS_MASTER = "refs/heads/master";
53  
54  	private static final String REFS_HEADS_LEFT = "refs/heads/left";
55  
56  	private static final String DISABLE_CHECK_BRANCH = "refs/heads/disabled_checked";
57  
58  	private static final String ENABLE_CHECKED_BRANCH = "refs/heads/enabled_checked";
59  
60  	private static final String ENABLED_CHECKED_GIF = "enabled_checked.gif";
61  
62  	public Git createRepositoryBinaryConflict(Consumer<Git> initialCommit,
63  			Consumer<Git> leftCommit, Consumer<Git> rightCommit)
64  			throws NoFilepatternException, GitAPIException, NoWorkTreeException,
65  			IOException {
66  		// Set up a git whith conflict commits on images
67  		Git git = new Git(db);
68  
69  		// First commit
70  		initialCommit.accept(git);
71  		git.add().addFilepattern(".").call();
72  		RevCommit firstCommit = git.commit().setAll(true)
73  				.setMessage("initial commit adding git attribute file").call();
74  
75  		// Create branch and add an icon Checked_Boxe (enabled_checked)
76  		createBranch(firstCommit, REFS_HEADS_LEFT);
77  		checkoutBranch(REFS_HEADS_LEFT);
78  		leftCommit.accept(git);
79  		git.add().addFilepattern(".").call();
80  		git.commit().setMessage("Left").call();
81  
82  		// Create a second branch from master Unchecked_Boxe
83  		checkoutBranch(REFS_HEADS_MASTER);
84  		createBranch(firstCommit, REFS_HEADS_RIGHT);
85  		checkoutBranch(REFS_HEADS_RIGHT);
86  		rightCommit.accept(git);
87  		git.add().addFilepattern(".").call();
88  		git.commit().setMessage("Right").call();
89  
90  		checkoutBranch(REFS_HEADS_LEFT);
91  		return git;
92  
93  	}
94  
95  	@Test
96  	public void mergeTextualFile_NoAttr() throws NoWorkTreeException,
97  			NoFilepatternException, GitAPIException, IOException {
98  		try (Git git = createRepositoryBinaryConflict(g -> {
99  			try {
100 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
101 			} catch (IOException e) {
102 				e.printStackTrace();
103 			}
104 		}, g -> {
105 			try {
106 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
107 			} catch (IOException e) {
108 				e.printStackTrace();
109 			}
110 		}, g -> {
111 			try {
112 				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
113 			} catch (IOException e) {
114 				e.printStackTrace();
115 			}
116 		})) {
117 			checkoutBranch(REFS_HEADS_LEFT);
118 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
119 
120 			MergeResult mergeResult = git.merge()
121 					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
122 					.call();
123 			assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
124 
125 			assertNull(mergeResult.getConflicts());
126 
127 			// Check that the image was not modified (not conflict marker added)
128 			String result = read(
129 					writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n"));
130 			assertEquals(result, read(git.getRepository().getWorkTree().toPath()
131 					.resolve("main.cat").toFile()));
132 		}
133 	}
134 
135 	@Test
136 	public void mergeTextualFile_UnsetMerge_Conflict()
137 			throws NoWorkTreeException, NoFilepatternException, GitAPIException,
138 			IOException {
139 		try (Git git = createRepositoryBinaryConflict(g -> {
140 			try {
141 				writeTrashFile(".gitattributes", "*.cat -merge");
142 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
143 			} catch (IOException e) {
144 				e.printStackTrace();
145 			}
146 		}, g -> {
147 			try {
148 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
149 			} catch (IOException e) {
150 				e.printStackTrace();
151 			}
152 		}, g -> {
153 			try {
154 				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
155 			} catch (IOException e) {
156 				e.printStackTrace();
157 			}
158 		})) {
159 			// Check that the merge attribute is unset
160 			assertAddMergeAttributeUnset(REFS_HEADS_LEFT, "main.cat");
161 			assertAddMergeAttributeUnset(REFS_HEADS_RIGHT, "main.cat");
162 
163 			checkoutBranch(REFS_HEADS_LEFT);
164 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
165 
166 			String catContent = read(git.getRepository().getWorkTree().toPath()
167 					.resolve("main.cat").toFile());
168 
169 			MergeResult mergeResult = git.merge()
170 					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
171 					.call();
172 			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
173 
174 			// Check that the image was not modified (not conflict marker added)
175 			assertEquals(catContent, read(git.getRepository().getWorkTree()
176 					.toPath().resolve("main.cat").toFile()));
177 		}
178 	}
179 
180 	@Test
181 	public void mergeTextualFile_UnsetMerge_NoConflict()
182 			throws NoWorkTreeException, NoFilepatternException, GitAPIException,
183 			IOException {
184 		try (Git git = createRepositoryBinaryConflict(g -> {
185 			try {
186 				writeTrashFile(".gitattributes", "*.txt -merge");
187 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
188 			} catch (IOException e) {
189 				e.printStackTrace();
190 			}
191 		}, g -> {
192 			try {
193 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
194 			} catch (IOException e) {
195 				e.printStackTrace();
196 			}
197 		}, g -> {
198 			try {
199 				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
200 			} catch (IOException e) {
201 				e.printStackTrace();
202 			}
203 		})) {
204 			// Check that the merge attribute is unset
205 			assertAddMergeAttributeUndefined(REFS_HEADS_LEFT, "main.cat");
206 			assertAddMergeAttributeUndefined(REFS_HEADS_RIGHT, "main.cat");
207 
208 			checkoutBranch(REFS_HEADS_LEFT);
209 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
210 
211 			MergeResult mergeResult = git.merge()
212 					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
213 					.call();
214 			assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
215 
216 			// Check that the image was not modified (not conflict marker added)
217 			String result = read(
218 					writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n"));
219 			assertEquals(result, read(git.getRepository().getWorkTree()
220 					.toPath().resolve("main.cat").toFile()));
221 		}
222 	}
223 
224 	@Test
225 	public void mergeTextualFile_SetBinaryMerge_Conflict()
226 			throws NoWorkTreeException, NoFilepatternException, GitAPIException,
227 			IOException {
228 		try (Git git = createRepositoryBinaryConflict(g -> {
229 			try {
230 				writeTrashFile(".gitattributes", "*.cat merge=binary");
231 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
232 			} catch (IOException e) {
233 				e.printStackTrace();
234 			}
235 		}, g -> {
236 			try {
237 				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
238 			} catch (IOException e) {
239 				e.printStackTrace();
240 			}
241 		}, g -> {
242 			try {
243 				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
244 			} catch (IOException e) {
245 				e.printStackTrace();
246 			}
247 		})) {
248 			// Check that the merge attribute is set to binary
249 			assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat",
250 					"binary");
251 			assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat",
252 					"binary");
253 
254 			checkoutBranch(REFS_HEADS_LEFT);
255 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
256 
257 			String catContent = read(git.getRepository().getWorkTree().toPath()
258 					.resolve("main.cat").toFile());
259 
260 			MergeResult mergeResult = git.merge()
261 					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
262 					.call();
263 			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
264 
265 			// Check that the image was not modified (not conflict marker added)
266 			assertEquals(catContent, read(git.getRepository().getWorkTree()
267 					.toPath().resolve("main.cat").toFile()));
268 		}
269 	}
270 
271 	/*
272 	 * This test is commented because JGit add conflict markers in binary files.
273 	 * cf. https://www.eclipse.org/forums/index.php/t/1086511/
274 	 */
275 	@Test
276 	@Ignore
277 	public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException,
278 			IOException, NoHeadException, ConcurrentRefUpdateException,
279 			CheckoutConflictException, InvalidMergeHeadsException,
280 			WrongRepositoryStateException, NoMessageException, GitAPIException {
281 
282 		RevCommit disableCheckedCommit;
283 		// Set up a git with conflict commits on images
284 		try (Git git = new Git(db)) {
285 			// First commit
286 			write(new File(db.getWorkTree(), ".gitattributes"), "");
287 			git.add().addFilepattern(".gitattributes").call();
288 			RevCommit firstCommit = git.commit()
289 					.setMessage("initial commit adding git attribute file")
290 					.call();
291 
292 			// Create branch and add an icon Checked_Boxe (enabled_checked)
293 			createBranch(firstCommit, ENABLE_CHECKED_BRANCH);
294 			checkoutBranch(ENABLE_CHECKED_BRANCH);
295 			copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, "");
296 			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
297 			git.commit().setMessage("enabled_checked commit").call();
298 
299 			// Create a second branch from master Unchecked_Boxe
300 			checkoutBranch(REFS_HEADS_MASTER);
301 			createBranch(firstCommit, DISABLE_CHECK_BRANCH);
302 			checkoutBranch(DISABLE_CHECK_BRANCH);
303 			copy("disabled_checked.gif", ENABLED_CHECKED_GIF, "");
304 			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
305 			disableCheckedCommit = git.commit()
306 					.setMessage("disabled_checked commit").call();
307 
308 			// Check that the merge attribute is unset
309 			assertAddMergeAttributeUndefined(ENABLE_CHECKED_BRANCH,
310 					ENABLED_CHECKED_GIF);
311 			assertAddMergeAttributeUndefined(DISABLE_CHECK_BRANCH,
312 					ENABLED_CHECKED_GIF);
313 
314 			checkoutBranch(ENABLE_CHECKED_BRANCH);
315 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
316 			MergeResult mergeResult = git.merge().include(disableCheckedCommit)
317 					.call();
318 			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
319 
320 			// Check that the image was not modified (no conflict marker added)
321 			try (FileInputStream mergeResultFile = new FileInputStream(
322 					db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF)
323 							.toFile())) {
324 				assertTrue(contentEquals(
325 						getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
326 						mergeResultFile));
327 			}
328 		}
329 	}
330 
331 	@Test
332 	public void mergeBinaryFile_UnsetMerge_Conflict()
333 			throws IllegalStateException,
334 			IOException, NoHeadException, ConcurrentRefUpdateException,
335 			CheckoutConflictException, InvalidMergeHeadsException,
336 			WrongRepositoryStateException, NoMessageException, GitAPIException {
337 
338 		RevCommit disableCheckedCommit;
339 		// Set up a git whith conflict commits on images
340 		try (Git git = new Git(db)) {
341 			// First commit
342 			write(new File(db.getWorkTree(), ".gitattributes"), "*.gif -merge");
343 			git.add().addFilepattern(".gitattributes").call();
344 			RevCommit firstCommit = git.commit()
345 					.setMessage("initial commit adding git attribute file")
346 					.call();
347 
348 			// Create branch and add an icon Checked_Boxe (enabled_checked)
349 			createBranch(firstCommit, ENABLE_CHECKED_BRANCH);
350 			checkoutBranch(ENABLE_CHECKED_BRANCH);
351 			copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, "");
352 			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
353 			git.commit().setMessage("enabled_checked commit").call();
354 
355 			// Create a second branch from master Unchecked_Boxe
356 			checkoutBranch(REFS_HEADS_MASTER);
357 			createBranch(firstCommit, DISABLE_CHECK_BRANCH);
358 			checkoutBranch(DISABLE_CHECK_BRANCH);
359 			copy("disabled_checked.gif", ENABLED_CHECKED_GIF, "");
360 			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
361 			disableCheckedCommit = git.commit()
362 					.setMessage("disabled_checked commit").call();
363 
364 			// Check that the merge attribute is unset
365 			assertAddMergeAttributeUnset(ENABLE_CHECKED_BRANCH,
366 					ENABLED_CHECKED_GIF);
367 			assertAddMergeAttributeUnset(DISABLE_CHECK_BRANCH,
368 					ENABLED_CHECKED_GIF);
369 
370 			checkoutBranch(ENABLE_CHECKED_BRANCH);
371 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
372 			MergeResult mergeResult = git.merge().include(disableCheckedCommit)
373 					.call();
374 			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
375 
376 			// Check that the image was not modified (not conflict marker added)
377 			try (FileInputStream mergeResultFile = new FileInputStream(
378 					db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF)
379 							.toFile())) {
380 				assertTrue(contentEquals(
381 						getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
382 						mergeResultFile));
383 			}
384 		}
385 	}
386 
387 	@Test
388 	public void mergeBinaryFile_SetMerge_Conflict()
389 			throws IllegalStateException, IOException, NoHeadException,
390 			ConcurrentRefUpdateException, CheckoutConflictException,
391 			InvalidMergeHeadsException, WrongRepositoryStateException,
392 			NoMessageException, GitAPIException {
393 
394 		RevCommit disableCheckedCommit;
395 		// Set up a git whith conflict commits on images
396 		try (Git git = new Git(db)) {
397 			// First commit
398 			write(new File(db.getWorkTree(), ".gitattributes"), "*.gif merge");
399 			git.add().addFilepattern(".gitattributes").call();
400 			RevCommit firstCommit = git.commit()
401 					.setMessage("initial commit adding git attribute file")
402 					.call();
403 
404 			// Create branch and add an icon Checked_Boxe (enabled_checked)
405 			createBranch(firstCommit, ENABLE_CHECKED_BRANCH);
406 			checkoutBranch(ENABLE_CHECKED_BRANCH);
407 			copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, "");
408 			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
409 			git.commit().setMessage("enabled_checked commit").call();
410 
411 			// Create a second branch from master Unchecked_Boxe
412 			checkoutBranch(REFS_HEADS_MASTER);
413 			createBranch(firstCommit, DISABLE_CHECK_BRANCH);
414 			checkoutBranch(DISABLE_CHECK_BRANCH);
415 			copy("disabled_checked.gif", ENABLED_CHECKED_GIF, "");
416 			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
417 			disableCheckedCommit = git.commit()
418 					.setMessage("disabled_checked commit").call();
419 
420 			// Check that the merge attribute is set
421 			assertAddMergeAttributeSet(ENABLE_CHECKED_BRANCH,
422 					ENABLED_CHECKED_GIF);
423 			assertAddMergeAttributeSet(DISABLE_CHECK_BRANCH,
424 					ENABLED_CHECKED_GIF);
425 
426 			checkoutBranch(ENABLE_CHECKED_BRANCH);
427 			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
428 			MergeResult mergeResult = git.merge().include(disableCheckedCommit)
429 					.call();
430 			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
431 
432 			// Check that the image was not modified (not conflict marker added)
433 			try (FileInputStream mergeResultFile = new FileInputStream(
434 					db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF)
435 							.toFile())) {
436 				assertFalse(contentEquals(
437 						getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
438 						mergeResultFile));
439 			}
440 		}
441 	}
442 
443 	/*
444 	 * Copied from org.apache.commons.io.IOUtils
445 	 */
446 	private boolean contentEquals(InputStream input1, InputStream input2)
447 			throws IOException {
448 		if (input1 == input2) {
449 			return true;
450 		}
451 		if (!(input1 instanceof BufferedInputStream)) {
452 			input1 = new BufferedInputStream(input1);
453 		}
454 		if (!(input2 instanceof BufferedInputStream)) {
455 			input2 = new BufferedInputStream(input2);
456 		}
457 
458 		int ch = input1.read();
459 		while (-1 != ch) {
460 			final int ch2 = input2.read();
461 			if (ch != ch2) {
462 				return false;
463 			}
464 			ch = input1.read();
465 		}
466 
467 		final int ch2 = input2.read();
468 		return ch2 == -1;
469 	}
470 
471 	private void assertAddMergeAttributeUnset(String branch, String fileName)
472 			throws IllegalStateException, IOException {
473 		checkoutBranch(branch);
474 
475 		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
476 			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
477 			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
478 
479 			assertTrue(treeWaklEnableChecked.next());
480 			Attributes attributes = treeWaklEnableChecked.getAttributes();
481 			Attribute mergeAttribute = attributes.get("merge");
482 			assertNotNull(mergeAttribute);
483 			assertEquals(Attribute.State.UNSET, mergeAttribute.getState());
484 		}
485 	}
486 
487 	private void assertAddMergeAttributeSet(String branch, String fileName)
488 			throws IllegalStateException, IOException {
489 		checkoutBranch(branch);
490 
491 		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
492 			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
493 			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
494 
495 			assertTrue(treeWaklEnableChecked.next());
496 			Attributes attributes = treeWaklEnableChecked.getAttributes();
497 			Attribute mergeAttribute = attributes.get("merge");
498 			assertNotNull(mergeAttribute);
499 			assertEquals(Attribute.State.SET, mergeAttribute.getState());
500 		}
501 	}
502 
503 	private void assertAddMergeAttributeUndefined(String branch,
504 			String fileName) throws IllegalStateException, IOException {
505 		checkoutBranch(branch);
506 
507 		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
508 			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
509 			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
510 
511 			assertTrue(treeWaklEnableChecked.next());
512 			Attributes attributes = treeWaklEnableChecked.getAttributes();
513 			Attribute mergeAttribute = attributes.get("merge");
514 			assertNull(mergeAttribute);
515 		}
516 	}
517 
518 	private void assertAddMergeAttributeCustom(String branch, String fileName,
519 			String value) throws IllegalStateException, IOException {
520 		checkoutBranch(branch);
521 
522 		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
523 			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
524 			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
525 
526 			assertTrue(treeWaklEnableChecked.next());
527 			Attributes attributes = treeWaklEnableChecked.getAttributes();
528 			Attribute mergeAttribute = attributes.get("merge");
529 			assertNotNull(mergeAttribute);
530 			assertEquals(Attribute.State.CUSTOM, mergeAttribute.getState());
531 			assertEquals(value, mergeAttribute.getValue());
532 		}
533 	}
534 
535 	private void copy(String resourcePath, String resourceNewName,
536 			String pathInRepo) throws IOException {
537 		InputStream input = getClass().getResourceAsStream(resourcePath);
538 		Files.copy(input, db.getWorkTree().toPath().resolve(pathInRepo)
539 				.resolve(resourceNewName));
540 	}
541 
542 }