View Javadoc
1   /*
2    * Copyright (C) 2012, 2020 Robin Stocker <robin@nibor.org> 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.merge;
11  
12  import static java.nio.charset.StandardCharsets.UTF_8;
13  import static java.time.Instant.EPOCH;
14  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
15  import static org.junit.Assert.assertEquals;
16  import static org.junit.Assert.assertFalse;
17  import static org.junit.Assert.assertNotNull;
18  import static org.junit.Assert.assertNull;
19  import static org.junit.Assert.assertTrue;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.IOException;
25  import java.time.Instant;
26  import java.util.Arrays;
27  import java.util.Map;
28  
29  import org.eclipse.jgit.api.Git;
30  import org.eclipse.jgit.api.MergeResult;
31  import org.eclipse.jgit.api.MergeResult.MergeStatus;
32  import org.eclipse.jgit.api.RebaseResult;
33  import org.eclipse.jgit.api.errors.CheckoutConflictException;
34  import org.eclipse.jgit.api.errors.GitAPIException;
35  import org.eclipse.jgit.api.errors.JGitInternalException;
36  import org.eclipse.jgit.dircache.DirCache;
37  import org.eclipse.jgit.dircache.DirCacheEditor;
38  import org.eclipse.jgit.dircache.DirCacheEntry;
39  import org.eclipse.jgit.errors.ConfigInvalidException;
40  import org.eclipse.jgit.errors.NoMergeBaseException;
41  import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
42  import org.eclipse.jgit.junit.RepositoryTestCase;
43  import org.eclipse.jgit.junit.TestRepository;
44  import org.eclipse.jgit.lib.AnyObjectId;
45  import org.eclipse.jgit.lib.ConfigConstants;
46  import org.eclipse.jgit.lib.Constants;
47  import org.eclipse.jgit.lib.FileMode;
48  import org.eclipse.jgit.lib.ObjectId;
49  import org.eclipse.jgit.lib.ObjectInserter;
50  import org.eclipse.jgit.lib.ObjectLoader;
51  import org.eclipse.jgit.lib.ObjectReader;
52  import org.eclipse.jgit.lib.ObjectStream;
53  import org.eclipse.jgit.lib.StoredConfig;
54  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
55  import org.eclipse.jgit.revwalk.RevCommit;
56  import org.eclipse.jgit.revwalk.RevObject;
57  import org.eclipse.jgit.revwalk.RevTree;
58  import org.eclipse.jgit.revwalk.RevWalk;
59  import org.eclipse.jgit.storage.file.FileBasedConfig;
60  import org.eclipse.jgit.treewalk.FileTreeIterator;
61  import org.eclipse.jgit.util.FS;
62  import org.eclipse.jgit.util.FileUtils;
63  import org.junit.Assert;
64  import org.junit.experimental.theories.DataPoints;
65  import org.junit.experimental.theories.Theories;
66  import org.junit.experimental.theories.Theory;
67  import org.junit.runner.RunWith;
68  
69  @RunWith(Theories.class)
70  public class MergerTest extends RepositoryTestCase {
71  
72  	@DataPoints
73  	public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] {
74  			MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE };
75  
76  	@Theory
77  	public void failingDeleteOfDirectoryWithUntrackedContent(
78  			MergeStrategy strategy) throws Exception {
79  		File folder1 = new File(db.getWorkTree(), "folder1");
80  		FileUtils.mkdir(folder1);
81  		File file = new File(folder1, "file1.txt");
82  		write(file, "folder1--file1.txt");
83  		file = new File(folder1, "file2.txt");
84  		write(file, "folder1--file2.txt");
85  
86  		try (Git git = new Git(db)) {
87  			git.add().addFilepattern(folder1.getName()).call();
88  			RevCommit base = git.commit().setMessage("adding folder").call();
89  
90  			recursiveDelete(folder1);
91  			git.rm().addFilepattern("folder1/file1.txt")
92  					.addFilepattern("folder1/file2.txt").call();
93  			RevCommit other = git.commit()
94  					.setMessage("removing folders on 'other'").call();
95  
96  			git.checkout().setName(base.name()).call();
97  
98  			file = new File(db.getWorkTree(), "unrelated.txt");
99  			write(file, "unrelated");
100 
101 			git.add().addFilepattern("unrelated.txt").call();
102 			RevCommit head = git.commit().setMessage("Adding another file").call();
103 
104 			// Untracked file to cause failing path for delete() of folder1
105 			// but that's ok.
106 			file = new File(folder1, "file3.txt");
107 			write(file, "folder1--file3.txt");
108 
109 			ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
110 			merger.setCommitNames(new String[] { "BASE", "HEAD", "other" });
111 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
112 			boolean ok = merger.merge(head.getId(), other.getId());
113 			assertTrue(ok);
114 			assertTrue(file.exists());
115 		}
116 	}
117 
118 	/**
119 	 * Merging two conflicting subtrees when the index does not contain any file
120 	 * in that subtree should lead to a conflicting state.
121 	 *
122 	 * @param strategy
123 	 * @throws Exception
124 	 */
125 	@Theory
126 	public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy)
127 			throws Exception {
128 		Git git = Git.wrap(db);
129 
130 		writeTrashFile("d/1", "orig");
131 		git.add().addFilepattern("d/1").call();
132 		RevCommit first = git.commit().setMessage("added d/1").call();
133 
134 		writeTrashFile("d/1", "master");
135 		RevCommit masterCommit = git.commit().setAll(true)
136 				.setMessage("modified d/1 on master").call();
137 
138 		git.checkout().setCreateBranch(true).setStartPoint(first)
139 				.setName("side").call();
140 		writeTrashFile("d/1", "side");
141 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
142 
143 		git.rm().addFilepattern("d/1").call();
144 		git.rm().addFilepattern("d").call();
145 		MergeResult mergeRes = git.merge().setStrategy(strategy)
146 				.include(masterCommit).call();
147 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
148 		assertEquals(
149 				"[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
150 				indexState(CONTENT));
151 	}
152 
153 	/**
154 	 * Merging two different but mergeable subtrees when the index does not
155 	 * contain any file in that subtree should lead to a merged state.
156 	 *
157 	 * @param strategy
158 	 * @throws Exception
159 	 */
160 	@Theory
161 	public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy)
162 			throws Exception {
163 		Git git = Git.wrap(db);
164 
165 		writeTrashFile("d/1", "1\n2\n3");
166 		git.add().addFilepattern("d/1").call();
167 		RevCommit first = git.commit().setMessage("added d/1").call();
168 
169 		writeTrashFile("d/1", "1master\n2\n3");
170 		RevCommit masterCommit = git.commit().setAll(true)
171 				.setMessage("modified d/1 on master").call();
172 
173 		git.checkout().setCreateBranch(true).setStartPoint(first)
174 				.setName("side").call();
175 		writeTrashFile("d/1", "1\n2\n3side");
176 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
177 
178 		git.rm().addFilepattern("d/1").call();
179 		git.rm().addFilepattern("d").call();
180 		MergeResult mergeRes = git.merge().setStrategy(strategy)
181 				.include(masterCommit).call();
182 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
183 		assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
184 				indexState(CONTENT));
185 	}
186 
187 	/**
188 	 * An existing directory without tracked content should not prevent merging
189 	 * a tree where that directory exists.
190 	 *
191 	 * @param strategy
192 	 * @throws Exception
193 	 */
194 	@Theory
195 	public void checkUntrackedFolderIsNotAConflict(
196 			MergeStrategy strategy) throws Exception {
197 		Git git = Git.wrap(db);
198 
199 		writeTrashFile("d/1", "1");
200 		git.add().addFilepattern("d/1").call();
201 		RevCommit first = git.commit().setMessage("added d/1").call();
202 
203 		writeTrashFile("e/1", "4");
204 		git.add().addFilepattern("e/1").call();
205 		RevCommit masterCommit = git.commit().setMessage("added e/1").call();
206 
207 		git.checkout().setCreateBranch(true).setStartPoint(first)
208 				.setName("side").call();
209 		writeTrashFile("f/1", "5");
210 		git.add().addFilepattern("f/1").call();
211 		git.commit().setAll(true).setMessage("added f/1")
212 				.call();
213 
214 		// Untracked directory e shall not conflict with merged e/1
215 		writeTrashFile("e/2", "d two");
216 
217 		MergeResult mergeRes = git.merge().setStrategy(strategy)
218 				.include(masterCommit).call();
219 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
220 		assertEquals(
221 				"[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]",
222 				indexState(CONTENT));
223 	}
224 
225 	/**
226 	 * A tracked file is replaced by a folder in THEIRS.
227 	 *
228 	 * @param strategy
229 	 * @throws Exception
230 	 */
231 	@Theory
232 	public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy)
233 			throws Exception {
234 		Git git = Git.wrap(db);
235 
236 		writeTrashFile("sub", "file");
237 		git.add().addFilepattern("sub").call();
238 		RevCommit first = git.commit().setMessage("initial").call();
239 
240 		git.checkout().setCreateBranch(true).setStartPoint(first)
241 				.setName("side").call();
242 
243 		git.rm().addFilepattern("sub").call();
244 		writeTrashFile("sub/file", "subfile");
245 		git.add().addFilepattern("sub/file").call();
246 		RevCommit masterCommit = git.commit().setMessage("file -> folder")
247 				.call();
248 
249 		git.checkout().setName("master").call();
250 		writeTrashFile("noop", "other");
251 		git.add().addFilepattern("noop").call();
252 		git.commit().setAll(true).setMessage("noop").call();
253 
254 		MergeResult mergeRes = git.merge().setStrategy(strategy)
255 				.include(masterCommit).call();
256 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
257 		assertEquals(
258 				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
259 				indexState(CONTENT));
260 	}
261 
262 	/**
263 	 * A tracked file is replaced by a folder in OURS.
264 	 *
265 	 * @param strategy
266 	 * @throws Exception
267 	 */
268 	@Theory
269 	public void checkFileReplacedByFolderInOurs(MergeStrategy strategy)
270 			throws Exception {
271 		Git git = Git.wrap(db);
272 
273 		writeTrashFile("sub", "file");
274 		git.add().addFilepattern("sub").call();
275 		RevCommit first = git.commit().setMessage("initial").call();
276 
277 		git.checkout().setCreateBranch(true).setStartPoint(first)
278 				.setName("side").call();
279 		writeTrashFile("noop", "other");
280 		git.add().addFilepattern("noop").call();
281 		RevCommit sideCommit = git.commit().setAll(true).setMessage("noop")
282 				.call();
283 
284 		git.checkout().setName("master").call();
285 		git.rm().addFilepattern("sub").call();
286 		writeTrashFile("sub/file", "subfile");
287 		git.add().addFilepattern("sub/file").call();
288 		git.commit().setMessage("file -> folder")
289 				.call();
290 
291 		MergeResult mergeRes = git.merge().setStrategy(strategy)
292 				.include(sideCommit).call();
293 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
294 		assertEquals(
295 				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
296 				indexState(CONTENT));
297 	}
298 
299 	/**
300 	 * An existing directory without tracked content should not prevent merging
301 	 * a file with that name.
302 	 *
303 	 * @param strategy
304 	 * @throws Exception
305 	 */
306 	@Theory
307 	public void checkUntrackedEmpytFolderIsNotAConflictWithFile(
308 			MergeStrategy strategy)
309 			throws Exception {
310 		Git git = Git.wrap(db);
311 
312 		writeTrashFile("d/1", "1");
313 		git.add().addFilepattern("d/1").call();
314 		RevCommit first = git.commit().setMessage("added d/1").call();
315 
316 		writeTrashFile("e", "4");
317 		git.add().addFilepattern("e").call();
318 		RevCommit masterCommit = git.commit().setMessage("added e").call();
319 
320 		git.checkout().setCreateBranch(true).setStartPoint(first)
321 				.setName("side").call();
322 		writeTrashFile("f/1", "5");
323 		git.add().addFilepattern("f/1").call();
324 		git.commit().setAll(true).setMessage("added f/1").call();
325 
326 		// Untracked empty directory hierarcy e/1 shall not conflict with merged
327 		// e/1
328 		FileUtils.mkdirs(new File(trash, "e/1"), true);
329 
330 		MergeResult mergeRes = git.merge().setStrategy(strategy)
331 				.include(masterCommit).call();
332 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
333 		assertEquals(
334 				"[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]",
335 				indexState(CONTENT));
336 	}
337 
338 	@Theory
339 	public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException,
340 			GitAPIException {
341 		Git git = Git.wrap(db);
342 		db.getConfig().setString("core", null, "autocrlf", "false");
343 		db.getConfig().save();
344 		writeTrashFile("crlf.txt", "some\r\ndata\r\n");
345 		git.add().addFilepattern("crlf.txt").call();
346 		git.commit().setMessage("base").call();
347 
348 		git.branchCreate().setName("brancha").call();
349 
350 		writeTrashFile("crlf.txt", "some\r\nmore\r\ndata\r\n");
351 		git.add().addFilepattern("crlf.txt").call();
352 		git.commit().setMessage("on master").call();
353 
354 		git.checkout().setName("brancha").call();
355 		writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n");
356 		git.add().addFilepattern("crlf.txt").call();
357 		git.commit().setMessage("on brancha").call();
358 
359 		db.getConfig().setString("core", null, "autocrlf", "input");
360 		db.getConfig().save();
361 
362 		MergeResult mergeResult = git.merge().setStrategy(strategy)
363 				.include(db.resolve("master"))
364 				.call();
365 		assertEquals(MergeResult.MergeStatus.MERGED,
366 				mergeResult.getMergeStatus());
367 	}
368 
369 	@Theory
370 	public void mergeConflictWithCrLfTextAuto(MergeStrategy strategy)
371 			throws IOException, GitAPIException {
372 		Git git = Git.wrap(db);
373 		writeTrashFile("crlf.txt", "a crlf file\r\n");
374 		git.add().addFilepattern("crlf.txt").call();
375 		git.commit().setMessage("base").call();
376 		assertEquals("[crlf.txt, mode:100644, content:a crlf file\r\n]",
377 				indexState(CONTENT));
378 		writeTrashFile(".gitattributes", "crlf.txt text=auto");
379 		git.add().addFilepattern(".gitattributes").call();
380 		git.commit().setMessage("attributes").call();
381 
382 		git.branchCreate().setName("brancha").call();
383 
384 		writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
385 		git.add().addFilepattern("crlf.txt").call();
386 		git.commit().setMessage("on master").call();
387 		assertEquals(
388 				"[.gitattributes, mode:100644, content:crlf.txt text=auto]"
389 						+ "[crlf.txt, mode:100644, content:a crlf file\r\na second line\r\n]",
390 				indexState(CONTENT));
391 
392 		git.checkout().setName("brancha").call();
393 		File testFile = writeTrashFile("crlf.txt",
394 				"a crlf file\r\nanother line\r\n");
395 		git.add().addFilepattern("crlf.txt").call();
396 		git.commit().setMessage("on brancha").call();
397 
398 		MergeResult mergeResult = git.merge().setStrategy(strategy)
399 				.include(db.resolve("master")).call();
400 		assertEquals(MergeResult.MergeStatus.CONFLICTING,
401 				mergeResult.getMergeStatus());
402 		checkFile(testFile,
403 				"a crlf file\r\n" //
404 						+ "<<<<<<< HEAD\n" //
405 						+ "another line\r\n" //
406 						+ "=======\n" //
407 						+ "a second line\r\n" //
408 						+ ">>>>>>> 8e9e704742f1bc8a41eac88aac4aeefd338b7384\n");
409 	}
410 
411 	@Theory
412 	public void mergeWithCrlfAutoCrlfTrue(MergeStrategy strategy)
413 			throws IOException, GitAPIException {
414 		Git git = Git.wrap(db);
415 		db.getConfig().setString("core", null, "autocrlf", "true");
416 		db.getConfig().save();
417 		writeTrashFile("crlf.txt", "a crlf file\r\n");
418 		git.add().addFilepattern("crlf.txt").call();
419 		git.commit().setMessage("base").call();
420 
421 		git.branchCreate().setName("brancha").call();
422 
423 		writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
424 		git.add().addFilepattern("crlf.txt").call();
425 		git.commit().setMessage("on master").call();
426 
427 		git.checkout().setName("brancha").call();
428 		File testFile = writeTrashFile("crlf.txt",
429 				"a first line\r\na crlf file\r\n");
430 		git.add().addFilepattern("crlf.txt").call();
431 		git.commit().setMessage("on brancha").call();
432 
433 		MergeResult mergeResult = git.merge().setStrategy(strategy)
434 				.include(db.resolve("master")).call();
435 		assertEquals(MergeResult.MergeStatus.MERGED,
436 				mergeResult.getMergeStatus());
437 		checkFile(testFile, "a first line\r\na crlf file\r\na second line\r\n");
438 		assertEquals(
439 				"[crlf.txt, mode:100644, content:a first line\na crlf file\na second line\n]",
440 				indexState(CONTENT));
441 	}
442 
443 	@Theory
444 	public void rebaseWithCrlfAutoCrlfTrue(MergeStrategy strategy)
445 			throws IOException, GitAPIException {
446 		Git git = Git.wrap(db);
447 		db.getConfig().setString("core", null, "autocrlf", "true");
448 		db.getConfig().save();
449 		writeTrashFile("crlf.txt", "line 1\r\nline 2\r\nline 3\r\n");
450 		git.add().addFilepattern("crlf.txt").call();
451 		RevCommit first = git.commit().setMessage("base").call();
452 
453 		git.checkout().setCreateBranch(true).setStartPoint(first)
454 				.setName("brancha").call();
455 
456 		File testFile = writeTrashFile("crlf.txt",
457 				"line 1\r\nmodified line\r\nline 3\r\n");
458 		git.add().addFilepattern("crlf.txt").call();
459 		git.commit().setMessage("on brancha").call();
460 
461 		git.checkout().setName("master").call();
462 		File otherFile = writeTrashFile("otherfile.txt", "a line\r\n");
463 		git.add().addFilepattern("otherfile.txt").call();
464 		git.commit().setMessage("on master").call();
465 
466 		git.checkout().setName("brancha").call();
467 		checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
468 		assertFalse(otherFile.exists());
469 
470 		RebaseResult rebaseResult = git.rebase().setStrategy(strategy)
471 				.setUpstream(db.resolve("master")).call();
472 		assertEquals(RebaseResult.Status.OK, rebaseResult.getStatus());
473 		checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
474 		checkFile(otherFile, "a line\r\n");
475 		assertEquals(
476 				"[crlf.txt, mode:100644, content:line 1\nmodified line\nline 3\n]"
477 						+ "[otherfile.txt, mode:100644, content:a line\n]",
478 				indexState(CONTENT));
479 	}
480 
481 	/**
482 	 * Merging two equal subtrees when the index does not contain any file in
483 	 * that subtree should lead to a merged state.
484 	 *
485 	 * @param strategy
486 	 * @throws Exception
487 	 */
488 	@Theory
489 	public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
490 			throws Exception {
491 		Git git = Git.wrap(db);
492 
493 		writeTrashFile("d/1", "orig");
494 		git.add().addFilepattern("d/1").call();
495 		RevCommit first = git.commit().setMessage("added d/1").call();
496 
497 		writeTrashFile("d/1", "modified");
498 		RevCommit masterCommit = git.commit().setAll(true)
499 				.setMessage("modified d/1 on master").call();
500 
501 		git.checkout().setCreateBranch(true).setStartPoint(first)
502 				.setName("side").call();
503 		writeTrashFile("d/1", "modified");
504 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
505 
506 		git.rm().addFilepattern("d/1").call();
507 		git.rm().addFilepattern("d").call();
508 		MergeResult mergeRes = git.merge().setStrategy(strategy)
509 				.include(masterCommit).call();
510 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
511 		assertEquals("[d/1, mode:100644, content:modified]",
512 				indexState(CONTENT));
513 	}
514 
515 	/**
516 	 * Merging two equal subtrees with an incore merger should lead to a merged
517 	 * state.
518 	 *
519 	 * @param strategy
520 	 * @throws Exception
521 	 */
522 	@Theory
523 	public void checkMergeEqualTreesInCore(MergeStrategy strategy)
524 			throws Exception {
525 		Git git = Git.wrap(db);
526 
527 		writeTrashFile("d/1", "orig");
528 		git.add().addFilepattern("d/1").call();
529 		RevCommit first = git.commit().setMessage("added d/1").call();
530 
531 		writeTrashFile("d/1", "modified");
532 		RevCommit masterCommit = git.commit().setAll(true)
533 				.setMessage("modified d/1 on master").call();
534 
535 		git.checkout().setCreateBranch(true).setStartPoint(first)
536 				.setName("side").call();
537 		writeTrashFile("d/1", "modified");
538 		RevCommit sideCommit = git.commit().setAll(true)
539 				.setMessage("modified d/1 on side").call();
540 
541 		git.rm().addFilepattern("d/1").call();
542 		git.rm().addFilepattern("d").call();
543 
544 		ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
545 				true);
546 		boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
547 		assertTrue(noProblems);
548 	}
549 
550 	/**
551 	 * Merging two equal subtrees with an incore merger should lead to a merged
552 	 * state, without using a Repository (the 'Gerrit' use case).
553 	 *
554 	 * @param strategy
555 	 * @throws Exception
556 	 */
557 	@Theory
558 	public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy)
559 			throws Exception {
560 		Git git = Git.wrap(db);
561 
562 		writeTrashFile("d/1", "orig");
563 		git.add().addFilepattern("d/1").call();
564 		RevCommit first = git.commit().setMessage("added d/1").call();
565 
566 		writeTrashFile("d/1", "modified");
567 		RevCommit masterCommit = git.commit().setAll(true)
568 				.setMessage("modified d/1 on master").call();
569 
570 		git.checkout().setCreateBranch(true).setStartPoint(first)
571 				.setName("side").call();
572 		writeTrashFile("d/1", "modified");
573 		RevCommit sideCommit = git.commit().setAll(true)
574 				.setMessage("modified d/1 on side").call();
575 
576 		git.rm().addFilepattern("d/1").call();
577 		git.rm().addFilepattern("d").call();
578 
579 		try (ObjectInserter ins = db.newObjectInserter()) {
580 			ThreeWayMerger resolveMerger =
581 					(ThreeWayMerger) strategy.newMerger(ins, db.getConfig());
582 			boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
583 			assertTrue(noProblems);
584 		}
585 	}
586 
587 	/**
588 	 * Merging two equal subtrees when the index and HEAD does not contain any
589 	 * file in that subtree should lead to a merged state.
590 	 *
591 	 * @param strategy
592 	 * @throws Exception
593 	 */
594 	@Theory
595 	public void checkMergeEqualNewTrees(MergeStrategy strategy)
596 			throws Exception {
597 		Git git = Git.wrap(db);
598 
599 		writeTrashFile("2", "orig");
600 		git.add().addFilepattern("2").call();
601 		RevCommit first = git.commit().setMessage("added 2").call();
602 
603 		writeTrashFile("d/1", "orig");
604 		git.add().addFilepattern("d/1").call();
605 		RevCommit masterCommit = git.commit().setAll(true)
606 				.setMessage("added d/1 on master").call();
607 
608 		git.checkout().setCreateBranch(true).setStartPoint(first)
609 				.setName("side").call();
610 		writeTrashFile("d/1", "orig");
611 		git.add().addFilepattern("d/1").call();
612 		git.commit().setAll(true).setMessage("added d/1 on side").call();
613 
614 		git.rm().addFilepattern("d/1").call();
615 		git.rm().addFilepattern("d").call();
616 		MergeResult mergeRes = git.merge().setStrategy(strategy)
617 				.include(masterCommit).call();
618 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
619 		assertEquals(
620 				"[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
621 				indexState(CONTENT));
622 	}
623 
624 	/**
625 	 * Merging two conflicting subtrees when the index and HEAD does not contain
626 	 * any file in that subtree should lead to a conflicting state.
627 	 *
628 	 * @param strategy
629 	 * @throws Exception
630 	 */
631 	@Theory
632 	public void checkMergeConflictingNewTrees(MergeStrategy strategy)
633 			throws Exception {
634 		Git git = Git.wrap(db);
635 
636 		writeTrashFile("2", "orig");
637 		git.add().addFilepattern("2").call();
638 		RevCommit first = git.commit().setMessage("added 2").call();
639 
640 		writeTrashFile("d/1", "master");
641 		git.add().addFilepattern("d/1").call();
642 		RevCommit masterCommit = git.commit().setAll(true)
643 				.setMessage("added d/1 on master").call();
644 
645 		git.checkout().setCreateBranch(true).setStartPoint(first)
646 				.setName("side").call();
647 		writeTrashFile("d/1", "side");
648 		git.add().addFilepattern("d/1").call();
649 		git.commit().setAll(true).setMessage("added d/1 on side").call();
650 
651 		git.rm().addFilepattern("d/1").call();
652 		git.rm().addFilepattern("d").call();
653 		MergeResult mergeRes = git.merge().setStrategy(strategy)
654 				.include(masterCommit).call();
655 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
656 		assertEquals(
657 				"[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
658 				indexState(CONTENT));
659 	}
660 
661 	/**
662 	 * Merging two conflicting files when the index contains a tree for that
663 	 * path should lead to a failed state.
664 	 *
665 	 * @param strategy
666 	 * @throws Exception
667 	 */
668 	@Theory
669 	public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
670 			throws Exception {
671 		Git git = Git.wrap(db);
672 
673 		writeTrashFile("0", "orig");
674 		git.add().addFilepattern("0").call();
675 		RevCommit first = git.commit().setMessage("added 0").call();
676 
677 		writeTrashFile("0", "master");
678 		RevCommit masterCommit = git.commit().setAll(true)
679 				.setMessage("modified 0 on master").call();
680 
681 		git.checkout().setCreateBranch(true).setStartPoint(first)
682 				.setName("side").call();
683 		writeTrashFile("0", "side");
684 		git.commit().setAll(true).setMessage("modified 0 on side").call();
685 
686 		git.rm().addFilepattern("0").call();
687 		writeTrashFile("0/0", "side");
688 		git.add().addFilepattern("0/0").call();
689 		MergeResult mergeRes = git.merge().setStrategy(strategy)
690 				.include(masterCommit).call();
691 		assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
692 	}
693 
694 	/**
695 	 * Merging two equal files when the index contains a tree for that path
696 	 * should lead to a failed state.
697 	 *
698 	 * @param strategy
699 	 * @throws Exception
700 	 */
701 	@Theory
702 	public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
703 			throws Exception {
704 		Git git = Git.wrap(db);
705 
706 		writeTrashFile("0", "orig");
707 		writeTrashFile("1", "1\n2\n3");
708 		git.add().addFilepattern("0").addFilepattern("1").call();
709 		RevCommit first = git.commit().setMessage("added 0, 1").call();
710 
711 		writeTrashFile("1", "1master\n2\n3");
712 		RevCommit masterCommit = git.commit().setAll(true)
713 				.setMessage("modified 1 on master").call();
714 
715 		git.checkout().setCreateBranch(true).setStartPoint(first)
716 				.setName("side").call();
717 		writeTrashFile("1", "1\n2\n3side");
718 		git.commit().setAll(true).setMessage("modified 1 on side").call();
719 
720 		git.rm().addFilepattern("0").call();
721 		writeTrashFile("0/0", "modified");
722 		git.add().addFilepattern("0/0").call();
723 		try {
724 			git.merge().setStrategy(strategy).include(masterCommit).call();
725 			Assert.fail("Didn't get the expected exception");
726 		} catch (CheckoutConflictException e) {
727 			assertEquals(1, e.getConflictingPaths().size());
728 			assertEquals("0/0", e.getConflictingPaths().get(0));
729 		}
730 	}
731 
732 	@Theory
733 	public void checkContentMergeNoConflict(MergeStrategy strategy)
734 			throws Exception {
735 		Git git = Git.wrap(db);
736 
737 		writeTrashFile("file", "1\n2\n3");
738 		git.add().addFilepattern("file").call();
739 		RevCommit first = git.commit().setMessage("added file").call();
740 
741 		writeTrashFile("file", "1master\n2\n3");
742 		git.commit().setAll(true).setMessage("modified file on master").call();
743 
744 		git.checkout().setCreateBranch(true).setStartPoint(first)
745 				.setName("side").call();
746 		writeTrashFile("file", "1\n2\n3side");
747 		RevCommit sideCommit = git.commit().setAll(true)
748 				.setMessage("modified file on side").call();
749 
750 		git.checkout().setName("master").call();
751 		MergeResult result =
752 				git.merge().setStrategy(strategy).include(sideCommit).call();
753 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
754 		String expected = "1master\n2\n3side";
755 		assertEquals(expected, read("file"));
756 	}
757 
758 	@Theory
759 	public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy)
760 			throws Exception {
761 		Git git = Git.wrap(db);
762 
763 		writeTrashFile("file", "1\n2\n3");
764 		git.add().addFilepattern("file").call();
765 		RevCommit first = git.commit().setMessage("added file").call();
766 
767 		writeTrashFile("file", "1master\n2\n3");
768 		RevCommit masterCommit = git.commit().setAll(true)
769 				.setMessage("modified file on master").call();
770 
771 		git.checkout().setCreateBranch(true).setStartPoint(first)
772 				.setName("side").call();
773 		writeTrashFile("file", "1\n2\n3side");
774 		RevCommit sideCommit = git.commit().setAll(true)
775 				.setMessage("modified file on side").call();
776 
777 		try (ObjectInserter ins = db.newObjectInserter()) {
778 			ResolveMerger merger =
779 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
780 			boolean noProblems = merger.merge(masterCommit, sideCommit);
781 			assertTrue(noProblems);
782 			assertEquals("1master\n2\n3side",
783 					readBlob(merger.getResultTreeId(), "file"));
784 		}
785 	}
786 
787 
788 	/**
789 	 * Merging a change involving large binary files should short-circuit reads.
790 	 *
791 	 * @param strategy
792 	 * @throws Exception
793 	 */
794 	@Theory
795 	public void checkContentMergeLargeBinaries(MergeStrategy strategy) throws Exception {
796 		Git git = Git.wrap(db);
797 		final int LINELEN = 72;
798 
799 		// setup a merge that would work correctly if we disconsider the stray '\0'
800 		// that the file contains near the start.
801 		byte[] binary = new byte[LINELEN * 2000];
802 		for (int i = 0; i < binary.length; i++) {
803 			binary[i] = (byte)((i % LINELEN) == 0 ? '\n' : 'x');
804 		}
805 		binary[50] = '\0';
806 
807 		writeTrashFile("file", new String(binary, UTF_8));
808 		git.add().addFilepattern("file").call();
809 		RevCommit first = git.commit().setMessage("added file").call();
810 
811 		// Generate an edit in a single line.
812 		int idx = LINELEN * 1200 + 1;
813 		byte save = binary[idx];
814 		binary[idx] = '@';
815 		writeTrashFile("file", new String(binary, UTF_8));
816 
817 		binary[idx] = save;
818 		git.add().addFilepattern("file").call();
819 		RevCommit masterCommit = git.commit().setAll(true)
820 			.setMessage("modified file l 1200").call();
821 
822 		git.checkout().setCreateBranch(true).setStartPoint(first).setName("side").call();
823 		binary[LINELEN * 1500 + 1] = '!';
824 		writeTrashFile("file", new String(binary, UTF_8));
825 		git.add().addFilepattern("file").call();
826 		RevCommit sideCommit = git.commit().setAll(true)
827 			.setMessage("modified file l 1500").call();
828 
829 		try (ObjectInserter ins = db.newObjectInserter()) {
830 			// Check that we don't read the large blobs.
831 			ObjectInserter forbidInserter = new ObjectInserter.Filter() {
832 				@Override
833 				protected ObjectInserter delegate() {
834 					return ins;
835 				}
836 
837 				@Override
838 				public ObjectReader newReader() {
839 					return new BigReadForbiddenReader(super.newReader(), 8000);
840 				}
841 			};
842 
843 			ResolveMerger merger =
844 				(ResolveMerger) strategy.newMerger(forbidInserter, db.getConfig());
845 			boolean noProblems = merger.merge(masterCommit, sideCommit);
846 			assertFalse(noProblems);
847 		}
848 	}
849 
850 	/**
851 	 * Throws an exception if reading beyond limit.
852 	 */
853 	static class BigReadForbiddenStream extends ObjectStream.Filter {
854 		long limit;
855 
856 		BigReadForbiddenStream(ObjectStream orig, long limit) {
857 			super(orig.getType(), orig.getSize(), orig);
858 			this.limit = limit;
859 		}
860 
861 		@Override
862 		public long skip(long n) throws IOException {
863 			limit -= n;
864 			if (limit < 0) {
865 				throw new IllegalStateException();
866 			}
867 
868 			return super.skip(n);
869 		}
870 
871 		@Override
872 		public int read() throws IOException {
873 			int r = super.read();
874 			limit--;
875 			if (limit < 0) {
876 				throw new IllegalStateException();
877 			}
878 			return r;
879 		}
880 
881 		@Override
882 		public int read(byte[] b, int off, int len) throws IOException {
883 			int n = super.read(b, off, len);
884 			limit -= n;
885 			if (limit < 0) {
886 				throw new IllegalStateException();
887 			}
888 			return n;
889 		}
890 	}
891 
892 	static class BigReadForbiddenReader extends ObjectReader.Filter {
893 		ObjectReader delegate;
894 		int limit;
895 
896 		@Override
897 		protected ObjectReader delegate() {
898 			return delegate;
899 		}
900 
901 		BigReadForbiddenReader(ObjectReader delegate, int limit) {
902 			this.delegate = delegate;
903 			this.limit = limit;
904 		}
905 
906 		@Override
907 		public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
908 			ObjectLoader orig = super.open(objectId, typeHint);
909 			return new ObjectLoader.Filter() {
910 				@Override
911 				protected ObjectLoader delegate() {
912 					return orig;
913 				}
914 
915 				@Override
916 				public ObjectStream openStream() throws IOException {
917 					ObjectStream os = orig.openStream();
918 					return new BigReadForbiddenStream(os, limit);
919 				}
920 			};
921 		}
922 	}
923 
924 	@Theory
925 	public void checkContentMergeConflict(MergeStrategy strategy)
926 			throws Exception {
927 		Git git = Git.wrap(db);
928 
929 		writeTrashFile("file", "1\n2\n3");
930 		git.add().addFilepattern("file").call();
931 		RevCommit first = git.commit().setMessage("added file").call();
932 
933 		writeTrashFile("file", "1master\n2\n3");
934 		git.commit().setAll(true).setMessage("modified file on master").call();
935 
936 		git.checkout().setCreateBranch(true).setStartPoint(first)
937 				.setName("side").call();
938 		writeTrashFile("file", "1side\n2\n3");
939 		RevCommit sideCommit = git.commit().setAll(true)
940 				.setMessage("modified file on side").call();
941 
942 		git.checkout().setName("master").call();
943 		MergeResult result =
944 				git.merge().setStrategy(strategy).include(sideCommit).call();
945 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
946 		String expected = "<<<<<<< HEAD\n"
947 				+ "1master\n"
948 				+ "=======\n"
949 				+ "1side\n"
950 				+ ">>>>>>> " + sideCommit.name() + "\n"
951 				+ "2\n"
952 				+ "3";
953 		assertEquals(expected, read("file"));
954 	}
955 
956 	@Theory
957 	public void checkContentMergeConflict_noTree(MergeStrategy strategy)
958 			throws Exception {
959 		Git git = Git.wrap(db);
960 
961 		writeTrashFile("file", "1\n2\n3");
962 		git.add().addFilepattern("file").call();
963 		RevCommit first = git.commit().setMessage("added file").call();
964 
965 		writeTrashFile("file", "1master\n2\n3");
966 		RevCommit masterCommit = git.commit().setAll(true)
967 				.setMessage("modified file on master").call();
968 
969 		git.checkout().setCreateBranch(true).setStartPoint(first)
970 				.setName("side").call();
971 		writeTrashFile("file", "1side\n2\n3");
972 		RevCommit sideCommit = git.commit().setAll(true)
973 				.setMessage("modified file on side").call();
974 
975 		try (ObjectInserter ins = db.newObjectInserter()) {
976 			ResolveMerger merger =
977 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
978 			boolean noProblems = merger.merge(masterCommit, sideCommit);
979 			assertFalse(noProblems);
980 			assertEquals(Arrays.asList("file"), merger.getUnmergedPaths());
981 
982 			MergeFormatter fmt = new MergeFormatter();
983 			merger.getMergeResults().get("file");
984 			try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
985 				fmt.formatMerge(out, merger.getMergeResults().get("file"),
986 						"BASE", "OURS", "THEIRS", UTF_8);
987 				String expected = "<<<<<<< OURS\n"
988 						+ "1master\n"
989 						+ "=======\n"
990 						+ "1side\n"
991 						+ ">>>>>>> THEIRS\n"
992 						+ "2\n"
993 						+ "3";
994 				assertEquals(expected, new String(out.toByteArray(), UTF_8));
995 			}
996 		}
997 	}
998 
999 	/**
1000 	 * Merging after criss-cross merges. In this case we merge together two
1001 	 * commits which have two equally good common ancestors
1002 	 *
1003 	 * @param strategy
1004 	 * @throws Exception
1005 	 */
1006 	@Theory
1007 	public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
1008 		Git git = Git.wrap(db);
1009 
1010 		writeTrashFile("1", "1\n2\n3");
1011 		git.add().addFilepattern("1").call();
1012 		RevCommit first = git.commit().setMessage("added 1").call();
1013 
1014 		writeTrashFile("1", "1master\n2\n3");
1015 		RevCommit masterCommit = git.commit().setAll(true)
1016 				.setMessage("modified 1 on master").call();
1017 
1018 		writeTrashFile("1", "1master2\n2\n3");
1019 		git.commit().setAll(true)
1020 				.setMessage("modified 1 on master again").call();
1021 
1022 		git.checkout().setCreateBranch(true).setStartPoint(first)
1023 				.setName("side").call();
1024 		writeTrashFile("1", "1\n2\na\nb\nc\n3side");
1025 		RevCommit sideCommit = git.commit().setAll(true)
1026 				.setMessage("modified 1 on side").call();
1027 
1028 		writeTrashFile("1", "1\n2\n3side2");
1029 		git.commit().setAll(true)
1030 				.setMessage("modified 1 on side again").call();
1031 
1032 		MergeResult result = git.merge().setStrategy(strategy)
1033 				.include(masterCommit).call();
1034 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
1035 		result.getNewHead();
1036 		git.checkout().setName("master").call();
1037 		result = git.merge().setStrategy(strategy).include(sideCommit).call();
1038 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
1039 
1040 		// we have two branches which are criss-cross merged. Try to merge the
1041 		// tips. This should succeed with RecursiveMerge and fail with
1042 		// ResolveMerge
1043 		try {
1044 			MergeResult mergeResult = git.merge().setStrategy(strategy)
1045 					.include(git.getRepository().exactRef("refs/heads/side"))
1046 					.call();
1047 			assertEquals(MergeStrategy.RECURSIVE, strategy);
1048 			assertEquals(MergeResult.MergeStatus.MERGED,
1049 					mergeResult.getMergeStatus());
1050 			assertEquals("1master2\n2\n3side2", read("1"));
1051 		} catch (JGitInternalException e) {
1052 			assertEquals(MergeStrategy.RESOLVE, strategy);
1053 			assertTrue(e.getCause() instanceof NoMergeBaseException);
1054 			assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
1055 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
1056 		}
1057 	}
1058 
1059 	@Theory
1060 	public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
1061 			throws Exception {
1062 		Git git = Git.wrap(db);
1063 
1064 		writeTrashFile("a.txt", "orig");
1065 		writeTrashFile("b.txt", "orig");
1066 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
1067 		RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
1068 
1069 		// modify and delete files on the master branch
1070 		writeTrashFile("a.txt", "master");
1071 		git.rm().addFilepattern("b.txt").call();
1072 		RevCommit masterCommit = git.commit()
1073 				.setMessage("modified a.txt, deleted b.txt").setAll(true)
1074 				.call();
1075 
1076 		// switch back to a side branch
1077 		git.checkout().setCreateBranch(true).setStartPoint(first)
1078 				.setName("side").call();
1079 		writeTrashFile("c.txt", "side");
1080 		git.add().addFilepattern("c.txt").call();
1081 		git.commit().setMessage("added c.txt").call();
1082 
1083 		// Get a handle to the file so on windows it can't be deleted.
1084 		try (FileInputStream fis = new FileInputStream(
1085 				new File(db.getWorkTree(), "b.txt"))) {
1086 			MergeResult mergeRes = git.merge().setStrategy(strategy)
1087 					.include(masterCommit).call();
1088 			if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
1089 				// probably windows
1090 				assertEquals(1, mergeRes.getFailingPaths().size());
1091 				assertEquals(MergeFailureReason.COULD_NOT_DELETE,
1092 						mergeRes.getFailingPaths().get("b.txt"));
1093 			}
1094 			assertEquals(
1095 					"[a.txt, mode:100644, content:master]"
1096 							+ "[c.txt, mode:100644, content:side]",
1097 					indexState(CONTENT));
1098 		}
1099 	}
1100 
1101 	@Theory
1102 	public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
1103 		File f;
1104 		Instant lastTs4, lastTsIndex;
1105 		Git git = Git.wrap(db);
1106 		File indexFile = db.getIndexFile();
1107 
1108 		// Create initial content and remember when the last file was written.
1109 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1110 		lastTs4 = FS.DETECTED.lastModifiedInstant(f);
1111 
1112 		// add all files, commit and check this doesn't update any working tree
1113 		// files and that the index is in a new file system timer tick. Make
1114 		// sure to wait long enough before adding so the index doesn't contain
1115 		// racily clean entries
1116 		fsTick(f);
1117 		git.add().addFilepattern(".").call();
1118 		RevCommit firstCommit = git.commit().setMessage("initial commit")
1119 				.call();
1120 		checkConsistentLastModified("0", "1", "2", "3", "4");
1121 		checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
1122 		assertEquals("Commit should not touch working tree file 4", lastTs4,
1123 				FS.DETECTED
1124 						.lastModifiedInstant(new File(db.getWorkTree(), "4")));
1125 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1126 
1127 		// Do modifications on the master branch. Then add and commit. This
1128 		// should touch only "0", "2 and "3"
1129 		fsTick(indexFile);
1130 		f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
1131 				null);
1132 		fsTick(f);
1133 		git.add().addFilepattern(".").call();
1134 		RevCommit masterCommit = git.commit().setMessage("master commit")
1135 				.call();
1136 		checkConsistentLastModified("0", "1", "2", "3", "4");
1137 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1138 				+ lastTsIndex, "<0", "2", "3", "<.git/index");
1139 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1140 
1141 		// Checkout a side branch. This should touch only "0", "2 and "3"
1142 		fsTick(indexFile);
1143 		git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
1144 				.setName("side").call();
1145 		checkConsistentLastModified("0", "1", "2", "3", "4");
1146 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1147 				+ lastTsIndex, "<0", "2", "3", ".git/index");
1148 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1149 
1150 		// This checkout may have populated worktree and index so fast that we
1151 		// may have smudged entries now. Check that we have the right content
1152 		// and then rewrite the index to get rid of smudged state
1153 		assertEquals("[0, mode:100644, content:orig]" //
1154 				+ "[1, mode:100644, content:orig]" //
1155 				+ "[2, mode:100644, content:1\n2\n3]" //
1156 				+ "[3, mode:100644, content:orig]" //
1157 				+ "[4, mode:100644, content:orig]", //
1158 				indexState(CONTENT));
1159 		fsTick(indexFile);
1160 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1161 		lastTs4 = FS.DETECTED.lastModifiedInstant(f);
1162 		fsTick(f);
1163 		git.add().addFilepattern(".").call();
1164 		checkConsistentLastModified("0", "1", "2", "3", "4");
1165 		checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
1166 				"4", "<.git/index");
1167 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1168 
1169 		// Do modifications on the side branch. Touch only "1", "2 and "3"
1170 		fsTick(indexFile);
1171 		f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
1172 		fsTick(f);
1173 		git.add().addFilepattern(".").call();
1174 		git.commit().setMessage("side commit").call();
1175 		checkConsistentLastModified("0", "1", "2", "3", "4");
1176 		checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
1177 				+ lastTsIndex, "<1", "2", "3", "<.git/index");
1178 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1179 
1180 		// merge master and side. Should only touch "0," "2" and "3"
1181 		fsTick(indexFile);
1182 		git.merge().setStrategy(strategy).include(masterCommit).call();
1183 		checkConsistentLastModified("0", "1", "2", "4");
1184 		checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
1185 				+ lastTsIndex, "<0", "2", "3", ".git/index");
1186 		assertEquals(
1187 				"[0, mode:100644, content:master]" //
1188 						+ "[1, mode:100644, content:side]" //
1189 						+ "[2, mode:100644, content:1master\n2\n3side]" //
1190 						+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
1191 						+ "[4, mode:100644, content:orig]", //
1192 				indexState(CONTENT));
1193 	}
1194 
1195 	/**
1196 	 * Merging two conflicting submodules when the index does not contain any
1197 	 * entry for that submodule.
1198 	 *
1199 	 * @param strategy
1200 	 * @throws Exception
1201 	 */
1202 	@Theory
1203 	public void checkMergeConflictingSubmodulesWithoutIndex(
1204 			MergeStrategy strategy) throws Exception {
1205 		Git git = Git.wrap(db);
1206 		writeTrashFile("initial", "initial");
1207 		git.add().addFilepattern("initial").call();
1208 		RevCommit initial = git.commit().setMessage("initial").call();
1209 
1210 		writeSubmodule("one", ObjectId
1211 				.fromString("1000000000000000000000000000000000000000"));
1212 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1213 		RevCommit right = git.commit().setMessage("added one").call();
1214 
1215 		// a second commit in the submodule
1216 
1217 		git.checkout().setStartPoint(initial).setName("left")
1218 				.setCreateBranch(true).call();
1219 		writeSubmodule("one", ObjectId
1220 				.fromString("2000000000000000000000000000000000000000"));
1221 
1222 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1223 		git.commit().setMessage("a different one").call();
1224 
1225 		MergeResult result = git.merge().setStrategy(strategy).include(right)
1226 				.call();
1227 
1228 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
1229 		Map<String, int[][]> conflicts = result.getConflicts();
1230 		assertEquals(1, conflicts.size());
1231 		assertNotNull(conflicts.get("one"));
1232 	}
1233 
1234 	/**
1235 	 * Merging two non-conflicting submodules when the index does not contain
1236 	 * any entry for either submodule.
1237 	 *
1238 	 * @param strategy
1239 	 * @throws Exception
1240 	 */
1241 	@Theory
1242 	public void checkMergeNonConflictingSubmodulesWithoutIndex(
1243 			MergeStrategy strategy) throws Exception {
1244 		Git git = Git.wrap(db);
1245 		writeTrashFile("initial", "initial");
1246 		git.add().addFilepattern("initial").call();
1247 
1248 		writeSubmodule("one", ObjectId
1249 				.fromString("1000000000000000000000000000000000000000"));
1250 
1251 		// Our initial commit should include a .gitmodules with a bunch of
1252 		// comment lines, so that
1253 		// we don't have a content merge issue when we add a new submodule at
1254 		// the top and a different
1255 		// one at the bottom. This is sort of a hack, but it should allow
1256 		// add/add submodule merges
1257 		String existing = read(Constants.DOT_GIT_MODULES);
1258 		String context = "\n# context\n# more context\n# yet more context\n";
1259 		write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1260 				existing + context + context + context);
1261 
1262 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1263 		RevCommit initial = git.commit().setMessage("initial").call();
1264 
1265 		writeSubmodule("two", ObjectId
1266 				.fromString("1000000000000000000000000000000000000000"));
1267 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1268 
1269 		RevCommit right = git.commit().setMessage("added two").call();
1270 
1271 		git.checkout().setStartPoint(initial).setName("left")
1272 				.setCreateBranch(true).call();
1273 
1274 		// we need to manually create the submodule for three for the
1275 		// .gitmodules hackery
1276 		addSubmoduleToIndex("three", ObjectId
1277 				.fromString("1000000000000000000000000000000000000000"));
1278 		new File(db.getWorkTree(), "three").mkdir();
1279 
1280 		existing = read(Constants.DOT_GIT_MODULES);
1281 		String three = "[submodule \"three\"]\n\tpath = three\n\turl = "
1282 				+ db.getDirectory().toURI() + "\n";
1283 		write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1284 				three + existing);
1285 
1286 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1287 		git.commit().setMessage("a different one").call();
1288 
1289 		MergeResult result = git.merge().setStrategy(strategy).include(right)
1290 				.call();
1291 
1292 		assertNull(result.getCheckoutConflicts());
1293 		assertNull(result.getFailingPaths());
1294 		for (String dir : Arrays.asList("one", "two", "three")) {
1295 			assertTrue(new File(db.getWorkTree(), dir).isDirectory());
1296 		}
1297 	}
1298 
1299 	/**
1300 	 * Merging two commits with a conflict in the virtual ancestor.
1301 	 *
1302 	 * Content conflicts while merging the virtual ancestor must be ignored.
1303 	 *
1304 	 * In the following tree, while merging A and B, the recursive algorithm
1305 	 * finds as base commits X and Y and tries to merge them: X deletes file "a"
1306 	 * and Y modifies it.
1307 	 *
1308 	 * Note: we delete "a" in (master) and (second-branch) to make avoid manual
1309 	 * merges. The situation is the same without those deletions and fixing
1310 	 * manually the merge of (merge-both-sides) on both branches.
1311 	 *
1312 	 * <pre>
1313 	 * A  (second-branch) Merge branch 'merge-both-sides' into second-branch
1314 	 * |\
1315 	 * o | Delete modified a
1316 	 * | |
1317 	 * | | B (master) Merge branch 'merge-both-sides' (into master)
1318 	 * | |/|
1319 	 * | X | (merge-both-sides) Delete original a
1320 	 * | | |
1321 	 * | | o Delete modified a
1322 	 * | |/
1323 	 * |/|
1324 	 * Y | Modify a
1325 	 * |/
1326 	 * o Initial commit
1327 	 * </pre>
1328 	 *
1329 	 * @param strategy
1330 	 * @throws Exception
1331 	 */
1332 	@Theory
1333 	public void checkMergeConflictInVirtualAncestor(
1334 			MergeStrategy strategy) throws Exception {
1335 		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1336 			return;
1337 		}
1338 
1339 		Git git = Git.wrap(db);
1340 
1341 		// master
1342 		writeTrashFile("a", "aaaaaaaa");
1343 		writeTrashFile("b", "bbbbbbbb");
1344 		git.add().addFilepattern("a").addFilepattern("b").call();
1345 		RevCommit first = git.commit().setMessage("Initial commit").call();
1346 
1347 		writeTrashFile("a", "aaaaaaaaaaaaaaa");
1348 		git.add().addFilepattern("a").call();
1349 		RevCommit commitY = git.commit().setMessage("Modify a").call();
1350 
1351 		git.rm().addFilepattern("a").call();
1352 		// Do more in this commits, so it is not identical to the deletion in
1353 		// second-branch
1354 		writeTrashFile("c", "cccccccc");
1355 		git.add().addFilepattern("c").call();
1356 		git.commit().setMessage("Delete modified a").call();
1357 
1358 		// merge-both-sides: starts before "a" is modified and deletes it
1359 		git.checkout().setCreateBranch(true).setStartPoint(first)
1360 				.setName("merge-both-sides").call();
1361 		git.rm().addFilepattern("a").call();
1362 		RevCommit commitX = git.commit().setMessage("Delete original a").call();
1363 
1364 		// second branch
1365 		git.checkout().setCreateBranch(true).setStartPoint(commitY)
1366 				.setName("second-branch").call();
1367 		git.rm().addFilepattern("a").call();
1368 		git.commit().setMessage("Delete modified a").call();
1369 
1370 		// Merge merge-both-sides into second-branch
1371 		MergeResult mergeResult = git.merge().include(commitX)
1372 				.setStrategy(strategy)
1373 				.call();
1374 		ObjectId commitB = mergeResult.getNewHead();
1375 
1376 		// Merge merge-both-sides into master
1377 		git.checkout().setName("master").call();
1378 		mergeResult = git.merge().include(commitX).setStrategy(strategy)
1379 				.call();
1380 
1381 		// Now, merge commit A and B (i.e. "master" and "second-branch").
1382 		// None of them have the file "a", so there is no conflict, BUT while
1383 		// building the virtual ancestor it will find a conflict between Y and X
1384 		git.merge().include(commitB).call();
1385 	}
1386 
1387 	/**
1388 	 * Merging two commits with a file/dir conflict in the virtual ancestor.
1389 	 *
1390 	 * <p>
1391 	 * Those conflicts should be ignored, otherwise the found base can not be used by the
1392 	 * RecursiveMerger.
1393 	 * <pre>
1394 	 *  --------------
1395 	 * |              \
1396 	 * |         C1 - C4 --- ?     master
1397 	 * |        /          /
1398 	 * |  I - A1 - C2 - C3         second-branch
1399 	 * |   \            /
1400 	 * \    \          /
1401 	 *  ----A2--------             branch-to-merge
1402 	 *  </pre>
1403 	 * <p>
1404 	 * <p>
1405 	 * Path "a" is initially a file in I and A1. It is changed to a directory in A2
1406 	 * ("branch-to-merge").
1407 	 * <p>
1408 	 * A2 is merged into "master" and "second-branch". The dir/file merge conflict is resolved
1409 	 * manually, results in C4 and C3.
1410 	 * <p>
1411 	 * While merging C3 and C4, A1 and A2 are the base commits found by the recursive merge that
1412 	 * have the dir/file conflict.
1413 	 */
1414 	@Theory
1415 	public void checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren(
1416 			MergeStrategy strategy)
1417 			throws Exception {
1418 		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1419 			return;
1420 		}
1421 
1422 		Git git = Git.wrap(db);
1423 
1424 		// master
1425 		writeTrashFile("a", "initial content");
1426 		git.add().addFilepattern("a").call();
1427 		RevCommit commitI = git.commit().setMessage("Initial commit").call();
1428 
1429 		writeTrashFile("a", "content in Ancestor 1");
1430 		git.add().addFilepattern("a").call();
1431 		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1432 
1433 		writeTrashFile("a", "content in Child 1 (commited on master)");
1434 		git.add().addFilepattern("a").call();
1435 		// commit C1M
1436 		git.commit().setMessage("Child 1 on master").call();
1437 
1438 		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1439 		// "a" becomes a directory in A2
1440 		git.rm().addFilepattern("a").call();
1441 		writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
1442 		git.add().addFilepattern("a/content").call();
1443 		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1444 
1445 		// second branch
1446 		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1447 		writeTrashFile("a", "content in Child 2 (commited on second-branch)");
1448 		git.add().addFilepattern("a").call();
1449 		// commit C2S
1450 		git.commit().setMessage("Child 2 on second-branch").call();
1451 
1452 		// Merge branch-to-merge into second-branch
1453 		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1454 		assertEquals(mergeResult.getNewHead(), null);
1455 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1456 		// Resolve the conflict manually, merge "a" as a file
1457 		git.rm().addFilepattern("a").call();
1458 		git.rm().addFilepattern("a/content").call();
1459 		writeTrashFile("a", "merge conflict resolution");
1460 		git.add().addFilepattern("a").call();
1461 		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
1462 				.call();
1463 
1464 		// Merge branch-to-merge into master
1465 		git.checkout().setName("master").call();
1466 		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1467 		assertEquals(mergeResult.getNewHead(), null);
1468 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1469 
1470 		// Resolve the conflict manually - merge "a" as a file
1471 		git.rm().addFilepattern("a").call();
1472 		git.rm().addFilepattern("a/content").call();
1473 		writeTrashFile("a", "merge conflict resolution");
1474 		git.add().addFilepattern("a").call();
1475 		// commit C4M
1476 		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1477 
1478 		// Merge C4M (second-branch) into master (C3S)
1479 		// Conflict in virtual base should be here, but there are no conflicts in
1480 		// children
1481 		mergeResult = git.merge().include(commitC3S).call();
1482 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
1483 
1484 	}
1485 
1486 	@Theory
1487 	public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_FileDir(MergeStrategy strategy)
1488 			throws Exception {
1489 		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1490 			return;
1491 		}
1492 
1493 		Git git = Git.wrap(db);
1494 
1495 		// master
1496 		writeTrashFile("a", "initial content");
1497 		git.add().addFilepattern("a").call();
1498 		RevCommit commitI = git.commit().setMessage("Initial commit").call();
1499 
1500 		writeTrashFile("a", "content in Ancestor 1");
1501 		git.add().addFilepattern("a").call();
1502 		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1503 
1504 		writeTrashFile("a", "content in Child 1 (commited on master)");
1505 		git.add().addFilepattern("a").call();
1506 		// commit C1M
1507 		git.commit().setMessage("Child 1 on master").call();
1508 
1509 		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1510 
1511 		// "a" becomes a directory in A2
1512 		git.rm().addFilepattern("a").call();
1513 		writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
1514 		git.add().addFilepattern("a/content").call();
1515 		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1516 
1517 		// second branch
1518 		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1519 		writeTrashFile("a", "content in Child 2 (commited on second-branch)");
1520 		git.add().addFilepattern("a").call();
1521 		// commit C2S
1522 		git.commit().setMessage("Child 2 on second-branch").call();
1523 
1524 		// Merge branch-to-merge into second-branch
1525 		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1526 		assertEquals(mergeResult.getNewHead(), null);
1527 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1528 		// Resolve the conflict manually - write a file
1529 		git.rm().addFilepattern("a").call();
1530 		git.rm().addFilepattern("a/content").call();
1531 		writeTrashFile("a",
1532 				"content in Child 3 (commited on second-branch) - merge conflict resolution");
1533 		git.add().addFilepattern("a").call();
1534 		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
1535 				.call();
1536 
1537 		// Merge branch-to-merge into master
1538 		git.checkout().setName("master").call();
1539 		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1540 		assertEquals(mergeResult.getNewHead(), null);
1541 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1542 
1543 		// Resolve the conflict manually - write a file
1544 		git.rm().addFilepattern("a").call();
1545 		git.rm().addFilepattern("a/content").call();
1546 		writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
1547 		git.add().addFilepattern("a").call();
1548 		// commit C4M
1549 		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1550 
1551 		// Merge C4M (second-branch) into master (C3S)
1552 		// Conflict in virtual base should be here
1553 		mergeResult = git.merge().include(commitC3S).call();
1554 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1555 		String expected =
1556 				"<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
1557 						+ "=======\n"
1558 						+ "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
1559 						+ ">>>>>>> " + commitC3S.name() + "\n";
1560 		assertEquals(expected, read("a"));
1561 		// Nothing was populated from the ancestors.
1562 		assertEquals(
1563 				"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
1564 				indexState(CONTENT));
1565 	}
1566 
1567 	/**
1568 	 * Same test as above, but "a" is a dir in A1 and a file in A2
1569 	 */
1570 	@Theory
1571 	public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFile(MergeStrategy strategy)
1572 			throws Exception {
1573 		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1574 			return;
1575 		}
1576 
1577 		Git git = Git.wrap(db);
1578 
1579 		// master
1580 		writeTrashFile("a/content", "initial content");
1581 		git.add().addFilepattern("a/content").call();
1582 		RevCommit commitI = git.commit().setMessage("Initial commit").call();
1583 
1584 		writeTrashFile("a/content", "content in Ancestor 1");
1585 		git.add().addFilepattern("a/content").call();
1586 		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1587 
1588 		writeTrashFile("a/content", "content in Child 1 (commited on master)");
1589 		git.add().addFilepattern("a/content").call();
1590 		// commit C1M
1591 		git.commit().setMessage("Child 1 on master").call();
1592 
1593 		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1594 
1595 		// "a" becomes a file in A2
1596 		git.rm().addFilepattern("a/content").call();
1597 		writeTrashFile("a", "content in Ancestor 2 (commited on branch-to-merge)");
1598 		git.add().addFilepattern("a").call();
1599 		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1600 
1601 		// second branch
1602 		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1603 		writeTrashFile("a/content", "content in Child 2 (commited on second-branch)");
1604 		git.add().addFilepattern("a/content").call();
1605 		// commit C2S
1606 		git.commit().setMessage("Child 2 on second-branch").call();
1607 
1608 		// Merge branch-to-merge into second-branch
1609 		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1610 		assertEquals(mergeResult.getNewHead(), null);
1611 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1612 		// Resolve the conflict manually - write a file
1613 		git.rm().addFilepattern("a").call();
1614 		git.rm().addFilepattern("a/content").call();
1615 		deleteTrashFile("a/content");
1616 		deleteTrashFile("a");
1617 		writeTrashFile("a", "content in Child 3 (commited on second-branch) - merge conflict resolution");
1618 		git.add().addFilepattern("a").call();
1619 		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
1620 
1621 		// Merge branch-to-merge into master
1622 		git.checkout().setName("master").call();
1623 		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1624 		assertEquals(mergeResult.getNewHead(), null);
1625 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1626 
1627 		// Resolve the conflict manually - write a file
1628 		git.rm().addFilepattern("a").call();
1629 		git.rm().addFilepattern("a/content").call();
1630 		deleteTrashFile("a/content");
1631 		deleteTrashFile("a");
1632 		writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
1633 		git.add().addFilepattern("a").call();
1634 		// commit C4M
1635 		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1636 
1637 		// Merge C4M (second-branch) into master (C3S)
1638 		// Conflict in virtual base should be here
1639 		mergeResult = git.merge().include(commitC3S).call();
1640 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1641 		String expected = "<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
1642 				+ "=======\n" + "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
1643 				+ ">>>>>>> " + commitC3S.name() + "\n";
1644 		assertEquals(expected, read("a"));
1645 		// Nothing was populated from the ancestors.
1646 		assertEquals(
1647 				"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
1648 				indexState(CONTENT));
1649 	}
1650 
1651 	/**
1652 	 * Merging two commits when files have equal content, but conflicting file mode
1653 	 * in the virtual ancestor.
1654 	 *
1655 	 * <p>
1656 	 * This test has the same set up as
1657 	 * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only
1658 	 * with the mode conflict in A1 and A2.
1659 	 */
1660 	@Theory
1661 	public void checkModeMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception {
1662 		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
1663 			return;
1664 		}
1665 
1666 		Git git = Git.wrap(db);
1667 
1668 		// master
1669 		writeTrashFile("c", "initial file");
1670 		git.add().addFilepattern("c").call();
1671 		RevCommit commitI = git.commit().setMessage("Initial commit").call();
1672 
1673 		File a = writeTrashFile("a", "content in Ancestor");
1674 		git.add().addFilepattern("a").call();
1675 		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
1676 
1677 		a = writeTrashFile("a", "content in Child 1 (commited on master)");
1678 		git.add().addFilepattern("a").call();
1679 		// commit C1M
1680 		git.commit().setMessage("Child 1 on master").call();
1681 
1682 		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
1683 		// "a" becomes executable in A2
1684 		a = writeTrashFile("a", "content in Ancestor");
1685 		a.setExecutable(true);
1686 		git.add().addFilepattern("a").call();
1687 		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
1688 
1689 		// second branch
1690 		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
1691 		a = writeTrashFile("a", "content in Child 2 (commited on second-branch)");
1692 		git.add().addFilepattern("a").call();
1693 		// commit C2S
1694 		git.commit().setMessage("Child 2 on second-branch").call();
1695 
1696 		// Merge branch-to-merge into second-branch
1697 		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1698 		assertEquals(mergeResult.getNewHead(), null);
1699 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1700 		// Resolve the conflict manually, merge "a" as non-executable
1701 		a = writeTrashFile("a", "merge conflict resolution");
1702 		a.setExecutable(false);
1703 		git.add().addFilepattern("a").call();
1704 		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
1705 
1706 		// Merge branch-to-merge into master
1707 		git.checkout().setName("master").call();
1708 		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
1709 		assertEquals(mergeResult.getNewHead(), null);
1710 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
1711 
1712 		// Resolve the conflict manually - merge "a" as non-executable
1713 		a = writeTrashFile("a", "merge conflict resolution");
1714 		a.setExecutable(false);
1715 		git.add().addFilepattern("a").call();
1716 		// commit C4M
1717 		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
1718 
1719 		// Merge C4M (second-branch) into master (C3S)
1720 		// Conflict in virtual base should be here, but there are no conflicts in
1721 		// children
1722 		mergeResult = git.merge().include(commitC3S).call();
1723 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
1724 
1725 	}
1726 
1727 	private void writeSubmodule(String path, ObjectId commit)
1728 			throws IOException, ConfigInvalidException {
1729 		addSubmoduleToIndex(path, commit);
1730 		new File(db.getWorkTree(), path).mkdir();
1731 
1732 		StoredConfig config = db.getConfig();
1733 		config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1734 				ConfigConstants.CONFIG_KEY_URL,
1735 				db.getDirectory().toURI().toString());
1736 		config.save();
1737 
1738 		FileBasedConfig modulesConfig = new FileBasedConfig(
1739 				new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1740 				db.getFS());
1741 		modulesConfig.load();
1742 		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1743 				ConfigConstants.CONFIG_KEY_PATH, path);
1744 		modulesConfig.save();
1745 
1746 	}
1747 
1748 	private void addSubmoduleToIndex(String path, ObjectId commit)
1749 			throws IOException {
1750 		DirCache cache = db.lockDirCache();
1751 		DirCacheEditor editor = cache.editor();
1752 		editor.add(new DirCacheEditor.PathEdit(path) {
1753 
1754 			@Override
1755 			public void apply(DirCacheEntry ent) {
1756 				ent.setFileMode(FileMode.GITLINK);
1757 				ent.setObjectId(commit);
1758 			}
1759 		});
1760 		editor.commit();
1761 	}
1762 
1763 	// Assert that every specified index entry has the same last modification
1764 	// timestamp as the associated file
1765 	private void checkConsistentLastModified(String... pathes)
1766 			throws IOException {
1767 		DirCache dc = db.readDirCache();
1768 		File workTree = db.getWorkTree();
1769 		for (String path : pathes)
1770 			assertEquals(
1771 					"IndexEntry with path "
1772 							+ path
1773 							+ " has lastmodified which is different from the worktree file",
1774 					FS.DETECTED.lastModifiedInstant(new File(workTree, path)),
1775 					dc.getEntry(path)
1776 							.getLastModifiedInstant());
1777 	}
1778 
1779 	// Assert that modification timestamps of working tree files are as
1780 	// expected. You may specify n files. It is asserted that every file
1781 	// i+1 is not older than file i. If a path of file i+1 is prefixed with "<"
1782 	// then this file must be younger then file i. A path "*<modtime>"
1783 	// represents a file with a modification time of <modtime>
1784 	// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
1785 	private void checkModificationTimeStampOrder(String... pathes) {
1786 		Instant lastMod = EPOCH;
1787 		for (String p : pathes) {
1788 			boolean strong = p.startsWith("<");
1789 			boolean fixed = p.charAt(strong ? 1 : 0) == '*';
1790 			p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
1791 			Instant curMod = fixed ? Instant.parse(p)
1792 					: FS.DETECTED
1793 							.lastModifiedInstant(new File(db.getWorkTree(), p));
1794 			if (strong) {
1795 				assertTrue("path " + p + " is not younger than predecesssor",
1796 						curMod.compareTo(lastMod) > 0);
1797 			} else {
1798 				assertTrue("path " + p + " is older than predecesssor",
1799 						curMod.compareTo(lastMod) >= 0);
1800 			}
1801 		}
1802 	}
1803 
1804 	private String readBlob(ObjectId treeish, String path) throws Exception {
1805 		try (TestRepository<?> tr = new TestRepository<>(db);
1806 				RevWalk rw = tr.getRevWalk()) {
1807 			RevTree tree = rw.parseTree(treeish);
1808 			RevObject obj = tr.get(tree, path);
1809 			if (obj == null) {
1810 				return null;
1811 			}
1812 			return new String(
1813 					rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(), UTF_8);
1814 		}
1815 	}
1816 }