View Javadoc
1   /*
2    * Copyright (C) 2012, 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 mergeWithCrlfAutoCrlfTrue(MergeStrategy strategy)
371 			throws IOException, GitAPIException {
372 		Git git = Git.wrap(db);
373 		db.getConfig().setString("core", null, "autocrlf", "true");
374 		db.getConfig().save();
375 		writeTrashFile("crlf.txt", "a crlf file\r\n");
376 		git.add().addFilepattern("crlf.txt").call();
377 		git.commit().setMessage("base").call();
378 
379 		git.branchCreate().setName("brancha").call();
380 
381 		writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
382 		git.add().addFilepattern("crlf.txt").call();
383 		git.commit().setMessage("on master").call();
384 
385 		git.checkout().setName("brancha").call();
386 		File testFile = writeTrashFile("crlf.txt",
387 				"a first line\r\na crlf file\r\n");
388 		git.add().addFilepattern("crlf.txt").call();
389 		git.commit().setMessage("on brancha").call();
390 
391 		MergeResult mergeResult = git.merge().setStrategy(strategy)
392 				.include(db.resolve("master")).call();
393 		assertEquals(MergeResult.MergeStatus.MERGED,
394 				mergeResult.getMergeStatus());
395 		checkFile(testFile, "a first line\r\na crlf file\r\na second line\r\n");
396 		assertEquals(
397 				"[crlf.txt, mode:100644, content:a first line\na crlf file\na second line\n]",
398 				indexState(CONTENT));
399 	}
400 
401 	@Theory
402 	public void rebaseWithCrlfAutoCrlfTrue(MergeStrategy strategy)
403 			throws IOException, GitAPIException {
404 		Git git = Git.wrap(db);
405 		db.getConfig().setString("core", null, "autocrlf", "true");
406 		db.getConfig().save();
407 		writeTrashFile("crlf.txt", "line 1\r\nline 2\r\nline 3\r\n");
408 		git.add().addFilepattern("crlf.txt").call();
409 		RevCommit first = git.commit().setMessage("base").call();
410 
411 		git.checkout().setCreateBranch(true).setStartPoint(first)
412 				.setName("brancha").call();
413 
414 		File testFile = writeTrashFile("crlf.txt",
415 				"line 1\r\nmodified line\r\nline 3\r\n");
416 		git.add().addFilepattern("crlf.txt").call();
417 		git.commit().setMessage("on brancha").call();
418 
419 		git.checkout().setName("master").call();
420 		File otherFile = writeTrashFile("otherfile.txt", "a line\r\n");
421 		git.add().addFilepattern("otherfile.txt").call();
422 		git.commit().setMessage("on master").call();
423 
424 		git.checkout().setName("brancha").call();
425 		checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
426 		assertFalse(otherFile.exists());
427 
428 		RebaseResult rebaseResult = git.rebase().setStrategy(strategy)
429 				.setUpstream(db.resolve("master")).call();
430 		assertEquals(RebaseResult.Status.OK, rebaseResult.getStatus());
431 		checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
432 		checkFile(otherFile, "a line\r\n");
433 		assertEquals(
434 				"[crlf.txt, mode:100644, content:line 1\nmodified line\nline 3\n]"
435 						+ "[otherfile.txt, mode:100644, content:a line\n]",
436 				indexState(CONTENT));
437 	}
438 
439 	/**
440 	 * Merging two equal subtrees when the index does not contain any file in
441 	 * that subtree should lead to a merged state.
442 	 *
443 	 * @param strategy
444 	 * @throws Exception
445 	 */
446 	@Theory
447 	public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
448 			throws Exception {
449 		Git git = Git.wrap(db);
450 
451 		writeTrashFile("d/1", "orig");
452 		git.add().addFilepattern("d/1").call();
453 		RevCommit first = git.commit().setMessage("added d/1").call();
454 
455 		writeTrashFile("d/1", "modified");
456 		RevCommit masterCommit = git.commit().setAll(true)
457 				.setMessage("modified d/1 on master").call();
458 
459 		git.checkout().setCreateBranch(true).setStartPoint(first)
460 				.setName("side").call();
461 		writeTrashFile("d/1", "modified");
462 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
463 
464 		git.rm().addFilepattern("d/1").call();
465 		git.rm().addFilepattern("d").call();
466 		MergeResult mergeRes = git.merge().setStrategy(strategy)
467 				.include(masterCommit).call();
468 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
469 		assertEquals("[d/1, mode:100644, content:modified]",
470 				indexState(CONTENT));
471 	}
472 
473 	/**
474 	 * Merging two equal subtrees with an incore merger should lead to a merged
475 	 * state.
476 	 *
477 	 * @param strategy
478 	 * @throws Exception
479 	 */
480 	@Theory
481 	public void checkMergeEqualTreesInCore(MergeStrategy strategy)
482 			throws Exception {
483 		Git git = Git.wrap(db);
484 
485 		writeTrashFile("d/1", "orig");
486 		git.add().addFilepattern("d/1").call();
487 		RevCommit first = git.commit().setMessage("added d/1").call();
488 
489 		writeTrashFile("d/1", "modified");
490 		RevCommit masterCommit = git.commit().setAll(true)
491 				.setMessage("modified d/1 on master").call();
492 
493 		git.checkout().setCreateBranch(true).setStartPoint(first)
494 				.setName("side").call();
495 		writeTrashFile("d/1", "modified");
496 		RevCommit sideCommit = git.commit().setAll(true)
497 				.setMessage("modified d/1 on side").call();
498 
499 		git.rm().addFilepattern("d/1").call();
500 		git.rm().addFilepattern("d").call();
501 
502 		ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
503 				true);
504 		boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
505 		assertTrue(noProblems);
506 	}
507 
508 	/**
509 	 * Merging two equal subtrees with an incore merger should lead to a merged
510 	 * state, without using a Repository (the 'Gerrit' use case).
511 	 *
512 	 * @param strategy
513 	 * @throws Exception
514 	 */
515 	@Theory
516 	public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy)
517 			throws Exception {
518 		Git git = Git.wrap(db);
519 
520 		writeTrashFile("d/1", "orig");
521 		git.add().addFilepattern("d/1").call();
522 		RevCommit first = git.commit().setMessage("added d/1").call();
523 
524 		writeTrashFile("d/1", "modified");
525 		RevCommit masterCommit = git.commit().setAll(true)
526 				.setMessage("modified d/1 on master").call();
527 
528 		git.checkout().setCreateBranch(true).setStartPoint(first)
529 				.setName("side").call();
530 		writeTrashFile("d/1", "modified");
531 		RevCommit sideCommit = git.commit().setAll(true)
532 				.setMessage("modified d/1 on side").call();
533 
534 		git.rm().addFilepattern("d/1").call();
535 		git.rm().addFilepattern("d").call();
536 
537 		try (ObjectInserter ins = db.newObjectInserter()) {
538 			ThreeWayMerger resolveMerger =
539 					(ThreeWayMerger) strategy.newMerger(ins, db.getConfig());
540 			boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
541 			assertTrue(noProblems);
542 		}
543 	}
544 
545 	/**
546 	 * Merging two equal subtrees when the index and HEAD does not contain any
547 	 * file in that subtree should lead to a merged state.
548 	 *
549 	 * @param strategy
550 	 * @throws Exception
551 	 */
552 	@Theory
553 	public void checkMergeEqualNewTrees(MergeStrategy strategy)
554 			throws Exception {
555 		Git git = Git.wrap(db);
556 
557 		writeTrashFile("2", "orig");
558 		git.add().addFilepattern("2").call();
559 		RevCommit first = git.commit().setMessage("added 2").call();
560 
561 		writeTrashFile("d/1", "orig");
562 		git.add().addFilepattern("d/1").call();
563 		RevCommit masterCommit = git.commit().setAll(true)
564 				.setMessage("added d/1 on master").call();
565 
566 		git.checkout().setCreateBranch(true).setStartPoint(first)
567 				.setName("side").call();
568 		writeTrashFile("d/1", "orig");
569 		git.add().addFilepattern("d/1").call();
570 		git.commit().setAll(true).setMessage("added d/1 on side").call();
571 
572 		git.rm().addFilepattern("d/1").call();
573 		git.rm().addFilepattern("d").call();
574 		MergeResult mergeRes = git.merge().setStrategy(strategy)
575 				.include(masterCommit).call();
576 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
577 		assertEquals(
578 				"[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
579 				indexState(CONTENT));
580 	}
581 
582 	/**
583 	 * Merging two conflicting subtrees when the index and HEAD does not contain
584 	 * any file in that subtree should lead to a conflicting state.
585 	 *
586 	 * @param strategy
587 	 * @throws Exception
588 	 */
589 	@Theory
590 	public void checkMergeConflictingNewTrees(MergeStrategy strategy)
591 			throws Exception {
592 		Git git = Git.wrap(db);
593 
594 		writeTrashFile("2", "orig");
595 		git.add().addFilepattern("2").call();
596 		RevCommit first = git.commit().setMessage("added 2").call();
597 
598 		writeTrashFile("d/1", "master");
599 		git.add().addFilepattern("d/1").call();
600 		RevCommit masterCommit = git.commit().setAll(true)
601 				.setMessage("added d/1 on master").call();
602 
603 		git.checkout().setCreateBranch(true).setStartPoint(first)
604 				.setName("side").call();
605 		writeTrashFile("d/1", "side");
606 		git.add().addFilepattern("d/1").call();
607 		git.commit().setAll(true).setMessage("added d/1 on side").call();
608 
609 		git.rm().addFilepattern("d/1").call();
610 		git.rm().addFilepattern("d").call();
611 		MergeResult mergeRes = git.merge().setStrategy(strategy)
612 				.include(masterCommit).call();
613 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
614 		assertEquals(
615 				"[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
616 				indexState(CONTENT));
617 	}
618 
619 	/**
620 	 * Merging two conflicting files when the index contains a tree for that
621 	 * path should lead to a failed state.
622 	 *
623 	 * @param strategy
624 	 * @throws Exception
625 	 */
626 	@Theory
627 	public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
628 			throws Exception {
629 		Git git = Git.wrap(db);
630 
631 		writeTrashFile("0", "orig");
632 		git.add().addFilepattern("0").call();
633 		RevCommit first = git.commit().setMessage("added 0").call();
634 
635 		writeTrashFile("0", "master");
636 		RevCommit masterCommit = git.commit().setAll(true)
637 				.setMessage("modified 0 on master").call();
638 
639 		git.checkout().setCreateBranch(true).setStartPoint(first)
640 				.setName("side").call();
641 		writeTrashFile("0", "side");
642 		git.commit().setAll(true).setMessage("modified 0 on side").call();
643 
644 		git.rm().addFilepattern("0").call();
645 		writeTrashFile("0/0", "side");
646 		git.add().addFilepattern("0/0").call();
647 		MergeResult mergeRes = git.merge().setStrategy(strategy)
648 				.include(masterCommit).call();
649 		assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
650 	}
651 
652 	/**
653 	 * Merging two equal files when the index contains a tree for that path
654 	 * should lead to a failed state.
655 	 *
656 	 * @param strategy
657 	 * @throws Exception
658 	 */
659 	@Theory
660 	public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
661 			throws Exception {
662 		Git git = Git.wrap(db);
663 
664 		writeTrashFile("0", "orig");
665 		writeTrashFile("1", "1\n2\n3");
666 		git.add().addFilepattern("0").addFilepattern("1").call();
667 		RevCommit first = git.commit().setMessage("added 0, 1").call();
668 
669 		writeTrashFile("1", "1master\n2\n3");
670 		RevCommit masterCommit = git.commit().setAll(true)
671 				.setMessage("modified 1 on master").call();
672 
673 		git.checkout().setCreateBranch(true).setStartPoint(first)
674 				.setName("side").call();
675 		writeTrashFile("1", "1\n2\n3side");
676 		git.commit().setAll(true).setMessage("modified 1 on side").call();
677 
678 		git.rm().addFilepattern("0").call();
679 		writeTrashFile("0/0", "modified");
680 		git.add().addFilepattern("0/0").call();
681 		try {
682 			git.merge().setStrategy(strategy).include(masterCommit).call();
683 			Assert.fail("Didn't get the expected exception");
684 		} catch (CheckoutConflictException e) {
685 			assertEquals(1, e.getConflictingPaths().size());
686 			assertEquals("0/0", e.getConflictingPaths().get(0));
687 		}
688 	}
689 
690 	@Theory
691 	public void checkContentMergeNoConflict(MergeStrategy strategy)
692 			throws Exception {
693 		Git git = Git.wrap(db);
694 
695 		writeTrashFile("file", "1\n2\n3");
696 		git.add().addFilepattern("file").call();
697 		RevCommit first = git.commit().setMessage("added file").call();
698 
699 		writeTrashFile("file", "1master\n2\n3");
700 		git.commit().setAll(true).setMessage("modified file on master").call();
701 
702 		git.checkout().setCreateBranch(true).setStartPoint(first)
703 				.setName("side").call();
704 		writeTrashFile("file", "1\n2\n3side");
705 		RevCommit sideCommit = git.commit().setAll(true)
706 				.setMessage("modified file on side").call();
707 
708 		git.checkout().setName("master").call();
709 		MergeResult result =
710 				git.merge().setStrategy(strategy).include(sideCommit).call();
711 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
712 		String expected = "1master\n2\n3side";
713 		assertEquals(expected, read("file"));
714 	}
715 
716 	@Theory
717 	public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy)
718 			throws Exception {
719 		Git git = Git.wrap(db);
720 
721 		writeTrashFile("file", "1\n2\n3");
722 		git.add().addFilepattern("file").call();
723 		RevCommit first = git.commit().setMessage("added file").call();
724 
725 		writeTrashFile("file", "1master\n2\n3");
726 		RevCommit masterCommit = git.commit().setAll(true)
727 				.setMessage("modified file on master").call();
728 
729 		git.checkout().setCreateBranch(true).setStartPoint(first)
730 				.setName("side").call();
731 		writeTrashFile("file", "1\n2\n3side");
732 		RevCommit sideCommit = git.commit().setAll(true)
733 				.setMessage("modified file on side").call();
734 
735 		try (ObjectInserter ins = db.newObjectInserter()) {
736 			ResolveMerger merger =
737 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
738 			boolean noProblems = merger.merge(masterCommit, sideCommit);
739 			assertTrue(noProblems);
740 			assertEquals("1master\n2\n3side",
741 					readBlob(merger.getResultTreeId(), "file"));
742 		}
743 	}
744 
745 
746 	/**
747 	 * Merging a change involving large binary files should short-circuit reads.
748 	 *
749 	 * @param strategy
750 	 * @throws Exception
751 	 */
752 	@Theory
753 	public void checkContentMergeLargeBinaries(MergeStrategy strategy) throws Exception {
754 		Git git = Git.wrap(db);
755 		final int LINELEN = 72;
756 
757 		// setup a merge that would work correctly if we disconsider the stray '\0'
758 		// that the file contains near the start.
759 		byte[] binary = new byte[LINELEN * 2000];
760 		for (int i = 0; i < binary.length; i++) {
761 			binary[i] = (byte)((i % LINELEN) == 0 ? '\n' : 'x');
762 		}
763 		binary[50] = '\0';
764 
765 		writeTrashFile("file", new String(binary, UTF_8));
766 		git.add().addFilepattern("file").call();
767 		RevCommit first = git.commit().setMessage("added file").call();
768 
769 		// Generate an edit in a single line.
770 		int idx = LINELEN * 1200 + 1;
771 		byte save = binary[idx];
772 		binary[idx] = '@';
773 		writeTrashFile("file", new String(binary, UTF_8));
774 
775 		binary[idx] = save;
776 		git.add().addFilepattern("file").call();
777 		RevCommit masterCommit = git.commit().setAll(true)
778 			.setMessage("modified file l 1200").call();
779 
780 		git.checkout().setCreateBranch(true).setStartPoint(first).setName("side").call();
781 		binary[LINELEN * 1500 + 1] = '!';
782 		writeTrashFile("file", new String(binary, UTF_8));
783 		git.add().addFilepattern("file").call();
784 		RevCommit sideCommit = git.commit().setAll(true)
785 			.setMessage("modified file l 1500").call();
786 
787 		try (ObjectInserter ins = db.newObjectInserter()) {
788 			// Check that we don't read the large blobs.
789 			ObjectInserter forbidInserter = new ObjectInserter.Filter() {
790 				@Override
791 				protected ObjectInserter delegate() {
792 					return ins;
793 				}
794 
795 				@Override
796 				public ObjectReader newReader() {
797 					return new BigReadForbiddenReader(super.newReader(), 8000);
798 				}
799 			};
800 
801 			ResolveMerger merger =
802 				(ResolveMerger) strategy.newMerger(forbidInserter, db.getConfig());
803 			boolean noProblems = merger.merge(masterCommit, sideCommit);
804 			assertFalse(noProblems);
805 		}
806 	}
807 
808 	/**
809 	 * Throws an exception if reading beyond limit.
810 	 */
811 	static class BigReadForbiddenStream extends ObjectStream.Filter {
812 		long limit;
813 
814 		BigReadForbiddenStream(ObjectStream orig, long limit) {
815 			super(orig.getType(), orig.getSize(), orig);
816 			this.limit = limit;
817 		}
818 
819 		@Override
820 		public long skip(long n) throws IOException {
821 			limit -= n;
822 			if (limit < 0) {
823 				throw new IllegalStateException();
824 			}
825 
826 			return super.skip(n);
827 		}
828 
829 		@Override
830 		public int read() throws IOException {
831 			int r = super.read();
832 			limit--;
833 			if (limit < 0) {
834 				throw new IllegalStateException();
835 			}
836 			return r;
837 		}
838 
839 		@Override
840 		public int read(byte[] b, int off, int len) throws IOException {
841 			int n = super.read(b, off, len);
842 			limit -= n;
843 			if (limit < 0) {
844 				throw new IllegalStateException();
845 			}
846 			return n;
847 		}
848 	}
849 
850 	static class BigReadForbiddenReader extends ObjectReader.Filter {
851 		ObjectReader delegate;
852 		int limit;
853 
854 		@Override
855 		protected ObjectReader delegate() {
856 			return delegate;
857 		}
858 
859 		BigReadForbiddenReader(ObjectReader delegate, int limit) {
860 			this.delegate = delegate;
861 			this.limit = limit;
862 		}
863 
864 		@Override
865 		public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
866 			ObjectLoader orig = super.open(objectId, typeHint);
867 			return new ObjectLoader.Filter() {
868 				@Override
869 				protected ObjectLoader delegate() {
870 					return orig;
871 				}
872 
873 				@Override
874 				public ObjectStream openStream() throws IOException {
875 					ObjectStream os = orig.openStream();
876 					return new BigReadForbiddenStream(os, limit);
877 				}
878 			};
879 		}
880 	}
881 
882 	@Theory
883 	public void checkContentMergeConflict(MergeStrategy strategy)
884 			throws Exception {
885 		Git git = Git.wrap(db);
886 
887 		writeTrashFile("file", "1\n2\n3");
888 		git.add().addFilepattern("file").call();
889 		RevCommit first = git.commit().setMessage("added file").call();
890 
891 		writeTrashFile("file", "1master\n2\n3");
892 		git.commit().setAll(true).setMessage("modified file on master").call();
893 
894 		git.checkout().setCreateBranch(true).setStartPoint(first)
895 				.setName("side").call();
896 		writeTrashFile("file", "1side\n2\n3");
897 		RevCommit sideCommit = git.commit().setAll(true)
898 				.setMessage("modified file on side").call();
899 
900 		git.checkout().setName("master").call();
901 		MergeResult result =
902 				git.merge().setStrategy(strategy).include(sideCommit).call();
903 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
904 		String expected = "<<<<<<< HEAD\n"
905 				+ "1master\n"
906 				+ "=======\n"
907 				+ "1side\n"
908 				+ ">>>>>>> " + sideCommit.name() + "\n"
909 				+ "2\n"
910 				+ "3";
911 		assertEquals(expected, read("file"));
912 	}
913 
914 	@Theory
915 	public void checkContentMergeConflict_noTree(MergeStrategy strategy)
916 			throws Exception {
917 		Git git = Git.wrap(db);
918 
919 		writeTrashFile("file", "1\n2\n3");
920 		git.add().addFilepattern("file").call();
921 		RevCommit first = git.commit().setMessage("added file").call();
922 
923 		writeTrashFile("file", "1master\n2\n3");
924 		RevCommit masterCommit = git.commit().setAll(true)
925 				.setMessage("modified file on master").call();
926 
927 		git.checkout().setCreateBranch(true).setStartPoint(first)
928 				.setName("side").call();
929 		writeTrashFile("file", "1side\n2\n3");
930 		RevCommit sideCommit = git.commit().setAll(true)
931 				.setMessage("modified file on side").call();
932 
933 		try (ObjectInserter ins = db.newObjectInserter()) {
934 			ResolveMerger merger =
935 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
936 			boolean noProblems = merger.merge(masterCommit, sideCommit);
937 			assertFalse(noProblems);
938 			assertEquals(Arrays.asList("file"), merger.getUnmergedPaths());
939 
940 			MergeFormatter fmt = new MergeFormatter();
941 			merger.getMergeResults().get("file");
942 			try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
943 				fmt.formatMerge(out, merger.getMergeResults().get("file"),
944 						"BASE", "OURS", "THEIRS", UTF_8);
945 				String expected = "<<<<<<< OURS\n"
946 						+ "1master\n"
947 						+ "=======\n"
948 						+ "1side\n"
949 						+ ">>>>>>> THEIRS\n"
950 						+ "2\n"
951 						+ "3";
952 				assertEquals(expected, new String(out.toByteArray(), UTF_8));
953 			}
954 		}
955 	}
956 
957 	/**
958 	 * Merging after criss-cross merges. In this case we merge together two
959 	 * commits which have two equally good common ancestors
960 	 *
961 	 * @param strategy
962 	 * @throws Exception
963 	 */
964 	@Theory
965 	public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
966 		Git git = Git.wrap(db);
967 
968 		writeTrashFile("1", "1\n2\n3");
969 		git.add().addFilepattern("1").call();
970 		RevCommit first = git.commit().setMessage("added 1").call();
971 
972 		writeTrashFile("1", "1master\n2\n3");
973 		RevCommit masterCommit = git.commit().setAll(true)
974 				.setMessage("modified 1 on master").call();
975 
976 		writeTrashFile("1", "1master2\n2\n3");
977 		git.commit().setAll(true)
978 				.setMessage("modified 1 on master again").call();
979 
980 		git.checkout().setCreateBranch(true).setStartPoint(first)
981 				.setName("side").call();
982 		writeTrashFile("1", "1\n2\na\nb\nc\n3side");
983 		RevCommit sideCommit = git.commit().setAll(true)
984 				.setMessage("modified 1 on side").call();
985 
986 		writeTrashFile("1", "1\n2\n3side2");
987 		git.commit().setAll(true)
988 				.setMessage("modified 1 on side again").call();
989 
990 		MergeResult result = git.merge().setStrategy(strategy)
991 				.include(masterCommit).call();
992 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
993 		result.getNewHead();
994 		git.checkout().setName("master").call();
995 		result = git.merge().setStrategy(strategy).include(sideCommit).call();
996 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
997 
998 		// we have two branches which are criss-cross merged. Try to merge the
999 		// tips. This should succeed with RecursiveMerge and fail with
1000 		// ResolveMerge
1001 		try {
1002 			MergeResult mergeResult = git.merge().setStrategy(strategy)
1003 					.include(git.getRepository().exactRef("refs/heads/side"))
1004 					.call();
1005 			assertEquals(MergeStrategy.RECURSIVE, strategy);
1006 			assertEquals(MergeResult.MergeStatus.MERGED,
1007 					mergeResult.getMergeStatus());
1008 			assertEquals("1master2\n2\n3side2", read("1"));
1009 		} catch (JGitInternalException e) {
1010 			assertEquals(MergeStrategy.RESOLVE, strategy);
1011 			assertTrue(e.getCause() instanceof NoMergeBaseException);
1012 			assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
1013 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
1014 		}
1015 	}
1016 
1017 	@Theory
1018 	public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
1019 			throws Exception {
1020 		Git git = Git.wrap(db);
1021 
1022 		writeTrashFile("a.txt", "orig");
1023 		writeTrashFile("b.txt", "orig");
1024 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
1025 		RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
1026 
1027 		// modify and delete files on the master branch
1028 		writeTrashFile("a.txt", "master");
1029 		git.rm().addFilepattern("b.txt").call();
1030 		RevCommit masterCommit = git.commit()
1031 				.setMessage("modified a.txt, deleted b.txt").setAll(true)
1032 				.call();
1033 
1034 		// switch back to a side branch
1035 		git.checkout().setCreateBranch(true).setStartPoint(first)
1036 				.setName("side").call();
1037 		writeTrashFile("c.txt", "side");
1038 		git.add().addFilepattern("c.txt").call();
1039 		git.commit().setMessage("added c.txt").call();
1040 
1041 		// Get a handle to the file so on windows it can't be deleted.
1042 		try (FileInputStream fis = new FileInputStream(
1043 				new File(db.getWorkTree(), "b.txt"))) {
1044 			MergeResult mergeRes = git.merge().setStrategy(strategy)
1045 					.include(masterCommit).call();
1046 			if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
1047 				// probably windows
1048 				assertEquals(1, mergeRes.getFailingPaths().size());
1049 				assertEquals(MergeFailureReason.COULD_NOT_DELETE,
1050 						mergeRes.getFailingPaths().get("b.txt"));
1051 			}
1052 			assertEquals(
1053 					"[a.txt, mode:100644, content:master]"
1054 							+ "[c.txt, mode:100644, content:side]",
1055 					indexState(CONTENT));
1056 		}
1057 	}
1058 
1059 	@Theory
1060 	public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
1061 		File f;
1062 		Instant lastTs4, lastTsIndex;
1063 		Git git = Git.wrap(db);
1064 		File indexFile = db.getIndexFile();
1065 
1066 		// Create initial content and remember when the last file was written.
1067 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1068 		lastTs4 = FS.DETECTED.lastModifiedInstant(f);
1069 
1070 		// add all files, commit and check this doesn't update any working tree
1071 		// files and that the index is in a new file system timer tick. Make
1072 		// sure to wait long enough before adding so the index doesn't contain
1073 		// racily clean entries
1074 		fsTick(f);
1075 		git.add().addFilepattern(".").call();
1076 		RevCommit firstCommit = git.commit().setMessage("initial commit")
1077 				.call();
1078 		checkConsistentLastModified("0", "1", "2", "3", "4");
1079 		checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
1080 		assertEquals("Commit should not touch working tree file 4", lastTs4,
1081 				FS.DETECTED
1082 						.lastModifiedInstant(new File(db.getWorkTree(), "4")));
1083 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1084 
1085 		// Do modifications on the master branch. Then add and commit. This
1086 		// should touch only "0", "2 and "3"
1087 		fsTick(indexFile);
1088 		f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
1089 				null);
1090 		fsTick(f);
1091 		git.add().addFilepattern(".").call();
1092 		RevCommit masterCommit = git.commit().setMessage("master commit")
1093 				.call();
1094 		checkConsistentLastModified("0", "1", "2", "3", "4");
1095 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1096 				+ lastTsIndex, "<0", "2", "3", "<.git/index");
1097 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1098 
1099 		// Checkout a side branch. This should touch only "0", "2 and "3"
1100 		fsTick(indexFile);
1101 		git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
1102 				.setName("side").call();
1103 		checkConsistentLastModified("0", "1", "2", "3", "4");
1104 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1105 				+ lastTsIndex, "<0", "2", "3", ".git/index");
1106 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1107 
1108 		// This checkout may have populated worktree and index so fast that we
1109 		// may have smudged entries now. Check that we have the right content
1110 		// and then rewrite the index to get rid of smudged state
1111 		assertEquals("[0, mode:100644, content:orig]" //
1112 				+ "[1, mode:100644, content:orig]" //
1113 				+ "[2, mode:100644, content:1\n2\n3]" //
1114 				+ "[3, mode:100644, content:orig]" //
1115 				+ "[4, mode:100644, content:orig]", //
1116 				indexState(CONTENT));
1117 		fsTick(indexFile);
1118 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1119 		lastTs4 = FS.DETECTED.lastModifiedInstant(f);
1120 		fsTick(f);
1121 		git.add().addFilepattern(".").call();
1122 		checkConsistentLastModified("0", "1", "2", "3", "4");
1123 		checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
1124 				"4", "<.git/index");
1125 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1126 
1127 		// Do modifications on the side branch. Touch only "1", "2 and "3"
1128 		fsTick(indexFile);
1129 		f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
1130 		fsTick(f);
1131 		git.add().addFilepattern(".").call();
1132 		git.commit().setMessage("side commit").call();
1133 		checkConsistentLastModified("0", "1", "2", "3", "4");
1134 		checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
1135 				+ lastTsIndex, "<1", "2", "3", "<.git/index");
1136 		lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
1137 
1138 		// merge master and side. Should only touch "0," "2" and "3"
1139 		fsTick(indexFile);
1140 		git.merge().setStrategy(strategy).include(masterCommit).call();
1141 		checkConsistentLastModified("0", "1", "2", "4");
1142 		checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
1143 				+ lastTsIndex, "<0", "2", "3", ".git/index");
1144 		assertEquals(
1145 				"[0, mode:100644, content:master]" //
1146 						+ "[1, mode:100644, content:side]" //
1147 						+ "[2, mode:100644, content:1master\n2\n3side]" //
1148 						+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
1149 						+ "[4, mode:100644, content:orig]", //
1150 				indexState(CONTENT));
1151 	}
1152 
1153 	/**
1154 	 * Merging two conflicting submodules when the index does not contain any
1155 	 * entry for that submodule.
1156 	 *
1157 	 * @param strategy
1158 	 * @throws Exception
1159 	 */
1160 	@Theory
1161 	public void checkMergeConflictingSubmodulesWithoutIndex(
1162 			MergeStrategy strategy) throws Exception {
1163 		Git git = Git.wrap(db);
1164 		writeTrashFile("initial", "initial");
1165 		git.add().addFilepattern("initial").call();
1166 		RevCommit initial = git.commit().setMessage("initial").call();
1167 
1168 		writeSubmodule("one", ObjectId
1169 				.fromString("1000000000000000000000000000000000000000"));
1170 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1171 		RevCommit right = git.commit().setMessage("added one").call();
1172 
1173 		// a second commit in the submodule
1174 
1175 		git.checkout().setStartPoint(initial).setName("left")
1176 				.setCreateBranch(true).call();
1177 		writeSubmodule("one", ObjectId
1178 				.fromString("2000000000000000000000000000000000000000"));
1179 
1180 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1181 		git.commit().setMessage("a different one").call();
1182 
1183 		MergeResult result = git.merge().setStrategy(strategy).include(right)
1184 				.call();
1185 
1186 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
1187 		Map<String, int[][]> conflicts = result.getConflicts();
1188 		assertEquals(1, conflicts.size());
1189 		assertNotNull(conflicts.get("one"));
1190 	}
1191 
1192 	/**
1193 	 * Merging two non-conflicting submodules when the index does not contain
1194 	 * any entry for either submodule.
1195 	 *
1196 	 * @param strategy
1197 	 * @throws Exception
1198 	 */
1199 	@Theory
1200 	public void checkMergeNonConflictingSubmodulesWithoutIndex(
1201 			MergeStrategy strategy) throws Exception {
1202 		Git git = Git.wrap(db);
1203 		writeTrashFile("initial", "initial");
1204 		git.add().addFilepattern("initial").call();
1205 
1206 		writeSubmodule("one", ObjectId
1207 				.fromString("1000000000000000000000000000000000000000"));
1208 
1209 		// Our initial commit should include a .gitmodules with a bunch of
1210 		// comment lines, so that
1211 		// we don't have a content merge issue when we add a new submodule at
1212 		// the top and a different
1213 		// one at the bottom. This is sort of a hack, but it should allow
1214 		// add/add submodule merges
1215 		String existing = read(Constants.DOT_GIT_MODULES);
1216 		String context = "\n# context\n# more context\n# yet more context\n";
1217 		write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1218 				existing + context + context + context);
1219 
1220 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1221 		RevCommit initial = git.commit().setMessage("initial").call();
1222 
1223 		writeSubmodule("two", ObjectId
1224 				.fromString("1000000000000000000000000000000000000000"));
1225 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1226 
1227 		RevCommit right = git.commit().setMessage("added two").call();
1228 
1229 		git.checkout().setStartPoint(initial).setName("left")
1230 				.setCreateBranch(true).call();
1231 
1232 		// we need to manually create the submodule for three for the
1233 		// .gitmodules hackery
1234 		addSubmoduleToIndex("three", ObjectId
1235 				.fromString("1000000000000000000000000000000000000000"));
1236 		new File(db.getWorkTree(), "three").mkdir();
1237 
1238 		existing = read(Constants.DOT_GIT_MODULES);
1239 		String three = "[submodule \"three\"]\n\tpath = three\n\turl = "
1240 				+ db.getDirectory().toURI() + "\n";
1241 		write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1242 				three + existing);
1243 
1244 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1245 		git.commit().setMessage("a different one").call();
1246 
1247 		MergeResult result = git.merge().setStrategy(strategy).include(right)
1248 				.call();
1249 
1250 		assertNull(result.getCheckoutConflicts());
1251 		assertNull(result.getFailingPaths());
1252 		for (String dir : Arrays.asList("one", "two", "three")) {
1253 			assertTrue(new File(db.getWorkTree(), dir).isDirectory());
1254 		}
1255 	}
1256 
1257 	private void writeSubmodule(String path, ObjectId commit)
1258 			throws IOException, ConfigInvalidException {
1259 		addSubmoduleToIndex(path, commit);
1260 		new File(db.getWorkTree(), path).mkdir();
1261 
1262 		StoredConfig config = db.getConfig();
1263 		config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1264 				ConfigConstants.CONFIG_KEY_URL,
1265 				db.getDirectory().toURI().toString());
1266 		config.save();
1267 
1268 		FileBasedConfig modulesConfig = new FileBasedConfig(
1269 				new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1270 				db.getFS());
1271 		modulesConfig.load();
1272 		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1273 				ConfigConstants.CONFIG_KEY_PATH, path);
1274 		modulesConfig.save();
1275 
1276 	}
1277 
1278 	private void addSubmoduleToIndex(String path, ObjectId commit)
1279 			throws IOException {
1280 		DirCache cache = db.lockDirCache();
1281 		DirCacheEditor editor = cache.editor();
1282 		editor.add(new DirCacheEditor.PathEdit(path) {
1283 
1284 			@Override
1285 			public void apply(DirCacheEntry ent) {
1286 				ent.setFileMode(FileMode.GITLINK);
1287 				ent.setObjectId(commit);
1288 			}
1289 		});
1290 		editor.commit();
1291 	}
1292 
1293 	// Assert that every specified index entry has the same last modification
1294 	// timestamp as the associated file
1295 	private void checkConsistentLastModified(String... pathes)
1296 			throws IOException {
1297 		DirCache dc = db.readDirCache();
1298 		File workTree = db.getWorkTree();
1299 		for (String path : pathes)
1300 			assertEquals(
1301 					"IndexEntry with path "
1302 							+ path
1303 							+ " has lastmodified which is different from the worktree file",
1304 					FS.DETECTED.lastModifiedInstant(new File(workTree, path)),
1305 					dc.getEntry(path)
1306 							.getLastModifiedInstant());
1307 	}
1308 
1309 	// Assert that modification timestamps of working tree files are as
1310 	// expected. You may specify n files. It is asserted that every file
1311 	// i+1 is not older than file i. If a path of file i+1 is prefixed with "<"
1312 	// then this file must be younger then file i. A path "*<modtime>"
1313 	// represents a file with a modification time of <modtime>
1314 	// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
1315 	private void checkModificationTimeStampOrder(String... pathes) {
1316 		Instant lastMod = EPOCH;
1317 		for (String p : pathes) {
1318 			boolean strong = p.startsWith("<");
1319 			boolean fixed = p.charAt(strong ? 1 : 0) == '*';
1320 			p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
1321 			Instant curMod = fixed ? Instant.parse(p)
1322 					: FS.DETECTED
1323 							.lastModifiedInstant(new File(db.getWorkTree(), p));
1324 			if (strong) {
1325 				assertTrue("path " + p + " is not younger than predecesssor",
1326 						curMod.compareTo(lastMod) > 0);
1327 			} else {
1328 				assertTrue("path " + p + " is older than predecesssor",
1329 						curMod.compareTo(lastMod) >= 0);
1330 			}
1331 		}
1332 	}
1333 
1334 	private String readBlob(ObjectId treeish, String path) throws Exception {
1335 		try (TestRepository<?> tr = new TestRepository<>(db);
1336 				RevWalk rw = tr.getRevWalk()) {
1337 			RevTree tree = rw.parseTree(treeish);
1338 			RevObject obj = tr.get(tree, path);
1339 			if (obj == null) {
1340 				return null;
1341 			}
1342 			return new String(
1343 					rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(), UTF_8);
1344 		}
1345 	}
1346 }