View Javadoc
1   /*
2    * Copyright (C) 2012, Robin Stocker <robin@nibor.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.merge;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
47  import static org.junit.Assert.assertEquals;
48  import static org.junit.Assert.assertFalse;
49  import static org.junit.Assert.assertNotNull;
50  import static org.junit.Assert.assertNull;
51  import static org.junit.Assert.assertTrue;
52  
53  import java.io.ByteArrayOutputStream;
54  import java.io.File;
55  import java.io.FileInputStream;
56  import java.io.IOException;
57  import java.util.Arrays;
58  import java.util.Map;
59  
60  import org.eclipse.jgit.api.Git;
61  import org.eclipse.jgit.api.MergeResult;
62  import org.eclipse.jgit.api.MergeResult.MergeStatus;
63  import org.eclipse.jgit.api.RebaseResult;
64  import org.eclipse.jgit.api.errors.CheckoutConflictException;
65  import org.eclipse.jgit.api.errors.GitAPIException;
66  import org.eclipse.jgit.api.errors.JGitInternalException;
67  import org.eclipse.jgit.dircache.DirCache;
68  import org.eclipse.jgit.dircache.DirCacheEditor;
69  import org.eclipse.jgit.dircache.DirCacheEntry;
70  import org.eclipse.jgit.errors.ConfigInvalidException;
71  import org.eclipse.jgit.errors.NoMergeBaseException;
72  import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
73  import org.eclipse.jgit.junit.RepositoryTestCase;
74  import org.eclipse.jgit.junit.TestRepository;
75  import org.eclipse.jgit.lib.AnyObjectId;
76  import org.eclipse.jgit.lib.ConfigConstants;
77  import org.eclipse.jgit.lib.Constants;
78  import org.eclipse.jgit.lib.FileMode;
79  import org.eclipse.jgit.lib.ObjectId;
80  import org.eclipse.jgit.lib.ObjectInserter;
81  import org.eclipse.jgit.lib.ObjectLoader;
82  import org.eclipse.jgit.lib.ObjectReader;
83  import org.eclipse.jgit.lib.ObjectStream;
84  import org.eclipse.jgit.lib.StoredConfig;
85  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
86  import org.eclipse.jgit.revwalk.RevCommit;
87  import org.eclipse.jgit.revwalk.RevObject;
88  import org.eclipse.jgit.revwalk.RevTree;
89  import org.eclipse.jgit.revwalk.RevWalk;
90  import org.eclipse.jgit.storage.file.FileBasedConfig;
91  import org.eclipse.jgit.treewalk.FileTreeIterator;
92  import org.eclipse.jgit.util.FS;
93  import org.eclipse.jgit.util.FileUtils;
94  import org.junit.Assert;
95  import org.junit.experimental.theories.DataPoints;
96  import org.junit.experimental.theories.Theories;
97  import org.junit.experimental.theories.Theory;
98  import org.junit.runner.RunWith;
99  
100 @RunWith(Theories.class)
101 public class MergerTest extends RepositoryTestCase {
102 
103 	@DataPoints
104 	public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] {
105 			MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE };
106 
107 	@Theory
108 	public void failingDeleteOfDirectoryWithUntrackedContent(
109 			MergeStrategy strategy) throws Exception {
110 		File folder1 = new File(db.getWorkTree(), "folder1");
111 		FileUtils.mkdir(folder1);
112 		File file = new File(folder1, "file1.txt");
113 		write(file, "folder1--file1.txt");
114 		file = new File(folder1, "file2.txt");
115 		write(file, "folder1--file2.txt");
116 
117 		try (Git git = new Git(db)) {
118 			git.add().addFilepattern(folder1.getName()).call();
119 			RevCommit base = git.commit().setMessage("adding folder").call();
120 
121 			recursiveDelete(folder1);
122 			git.rm().addFilepattern("folder1/file1.txt")
123 					.addFilepattern("folder1/file2.txt").call();
124 			RevCommit other = git.commit()
125 					.setMessage("removing folders on 'other'").call();
126 
127 			git.checkout().setName(base.name()).call();
128 
129 			file = new File(db.getWorkTree(), "unrelated.txt");
130 			write(file, "unrelated");
131 
132 			git.add().addFilepattern("unrelated.txt").call();
133 			RevCommit head = git.commit().setMessage("Adding another file").call();
134 
135 			// Untracked file to cause failing path for delete() of folder1
136 			// but that's ok.
137 			file = new File(folder1, "file3.txt");
138 			write(file, "folder1--file3.txt");
139 
140 			ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
141 			merger.setCommitNames(new String[] { "BASE", "HEAD", "other" });
142 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
143 			boolean ok = merger.merge(head.getId(), other.getId());
144 			assertTrue(ok);
145 			assertTrue(file.exists());
146 		}
147 	}
148 
149 	/**
150 	 * Merging two conflicting subtrees when the index does not contain any file
151 	 * in that subtree should lead to a conflicting state.
152 	 *
153 	 * @param strategy
154 	 * @throws Exception
155 	 */
156 	@Theory
157 	public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy)
158 			throws Exception {
159 		Git git = Git.wrap(db);
160 
161 		writeTrashFile("d/1", "orig");
162 		git.add().addFilepattern("d/1").call();
163 		RevCommit first = git.commit().setMessage("added d/1").call();
164 
165 		writeTrashFile("d/1", "master");
166 		RevCommit masterCommit = git.commit().setAll(true)
167 				.setMessage("modified d/1 on master").call();
168 
169 		git.checkout().setCreateBranch(true).setStartPoint(first)
170 				.setName("side").call();
171 		writeTrashFile("d/1", "side");
172 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
173 
174 		git.rm().addFilepattern("d/1").call();
175 		git.rm().addFilepattern("d").call();
176 		MergeResult mergeRes = git.merge().setStrategy(strategy)
177 				.include(masterCommit).call();
178 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
179 		assertEquals(
180 				"[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
181 				indexState(CONTENT));
182 	}
183 
184 	/**
185 	 * Merging two different but mergeable subtrees when the index does not
186 	 * contain any file in that subtree should lead to a merged state.
187 	 *
188 	 * @param strategy
189 	 * @throws Exception
190 	 */
191 	@Theory
192 	public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy)
193 			throws Exception {
194 		Git git = Git.wrap(db);
195 
196 		writeTrashFile("d/1", "1\n2\n3");
197 		git.add().addFilepattern("d/1").call();
198 		RevCommit first = git.commit().setMessage("added d/1").call();
199 
200 		writeTrashFile("d/1", "1master\n2\n3");
201 		RevCommit masterCommit = git.commit().setAll(true)
202 				.setMessage("modified d/1 on master").call();
203 
204 		git.checkout().setCreateBranch(true).setStartPoint(first)
205 				.setName("side").call();
206 		writeTrashFile("d/1", "1\n2\n3side");
207 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
208 
209 		git.rm().addFilepattern("d/1").call();
210 		git.rm().addFilepattern("d").call();
211 		MergeResult mergeRes = git.merge().setStrategy(strategy)
212 				.include(masterCommit).call();
213 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
214 		assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
215 				indexState(CONTENT));
216 	}
217 
218 	/**
219 	 * An existing directory without tracked content should not prevent merging
220 	 * a tree where that directory exists.
221 	 *
222 	 * @param strategy
223 	 * @throws Exception
224 	 */
225 	@Theory
226 	public void checkUntrackedFolderIsNotAConflict(
227 			MergeStrategy strategy) throws Exception {
228 		Git git = Git.wrap(db);
229 
230 		writeTrashFile("d/1", "1");
231 		git.add().addFilepattern("d/1").call();
232 		RevCommit first = git.commit().setMessage("added d/1").call();
233 
234 		writeTrashFile("e/1", "4");
235 		git.add().addFilepattern("e/1").call();
236 		RevCommit masterCommit = git.commit().setMessage("added e/1").call();
237 
238 		git.checkout().setCreateBranch(true).setStartPoint(first)
239 				.setName("side").call();
240 		writeTrashFile("f/1", "5");
241 		git.add().addFilepattern("f/1").call();
242 		git.commit().setAll(true).setMessage("added f/1")
243 				.call();
244 
245 		// Untracked directory e shall not conflict with merged e/1
246 		writeTrashFile("e/2", "d two");
247 
248 		MergeResult mergeRes = git.merge().setStrategy(strategy)
249 				.include(masterCommit).call();
250 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
251 		assertEquals(
252 				"[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]",
253 				indexState(CONTENT));
254 	}
255 
256 	/**
257 	 * A tracked file is replaced by a folder in THEIRS.
258 	 *
259 	 * @param strategy
260 	 * @throws Exception
261 	 */
262 	@Theory
263 	public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy)
264 			throws Exception {
265 		Git git = Git.wrap(db);
266 
267 		writeTrashFile("sub", "file");
268 		git.add().addFilepattern("sub").call();
269 		RevCommit first = git.commit().setMessage("initial").call();
270 
271 		git.checkout().setCreateBranch(true).setStartPoint(first)
272 				.setName("side").call();
273 
274 		git.rm().addFilepattern("sub").call();
275 		writeTrashFile("sub/file", "subfile");
276 		git.add().addFilepattern("sub/file").call();
277 		RevCommit masterCommit = git.commit().setMessage("file -> folder")
278 				.call();
279 
280 		git.checkout().setName("master").call();
281 		writeTrashFile("noop", "other");
282 		git.add().addFilepattern("noop").call();
283 		git.commit().setAll(true).setMessage("noop").call();
284 
285 		MergeResult mergeRes = git.merge().setStrategy(strategy)
286 				.include(masterCommit).call();
287 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
288 		assertEquals(
289 				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
290 				indexState(CONTENT));
291 	}
292 
293 	/**
294 	 * A tracked file is replaced by a folder in OURS.
295 	 *
296 	 * @param strategy
297 	 * @throws Exception
298 	 */
299 	@Theory
300 	public void checkFileReplacedByFolderInOurs(MergeStrategy strategy)
301 			throws Exception {
302 		Git git = Git.wrap(db);
303 
304 		writeTrashFile("sub", "file");
305 		git.add().addFilepattern("sub").call();
306 		RevCommit first = git.commit().setMessage("initial").call();
307 
308 		git.checkout().setCreateBranch(true).setStartPoint(first)
309 				.setName("side").call();
310 		writeTrashFile("noop", "other");
311 		git.add().addFilepattern("noop").call();
312 		RevCommit sideCommit = git.commit().setAll(true).setMessage("noop")
313 				.call();
314 
315 		git.checkout().setName("master").call();
316 		git.rm().addFilepattern("sub").call();
317 		writeTrashFile("sub/file", "subfile");
318 		git.add().addFilepattern("sub/file").call();
319 		git.commit().setMessage("file -> folder")
320 				.call();
321 
322 		MergeResult mergeRes = git.merge().setStrategy(strategy)
323 				.include(sideCommit).call();
324 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
325 		assertEquals(
326 				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
327 				indexState(CONTENT));
328 	}
329 
330 	/**
331 	 * An existing directory without tracked content should not prevent merging
332 	 * a file with that name.
333 	 *
334 	 * @param strategy
335 	 * @throws Exception
336 	 */
337 	@Theory
338 	public void checkUntrackedEmpytFolderIsNotAConflictWithFile(
339 			MergeStrategy strategy)
340 			throws Exception {
341 		Git git = Git.wrap(db);
342 
343 		writeTrashFile("d/1", "1");
344 		git.add().addFilepattern("d/1").call();
345 		RevCommit first = git.commit().setMessage("added d/1").call();
346 
347 		writeTrashFile("e", "4");
348 		git.add().addFilepattern("e").call();
349 		RevCommit masterCommit = git.commit().setMessage("added e").call();
350 
351 		git.checkout().setCreateBranch(true).setStartPoint(first)
352 				.setName("side").call();
353 		writeTrashFile("f/1", "5");
354 		git.add().addFilepattern("f/1").call();
355 		git.commit().setAll(true).setMessage("added f/1").call();
356 
357 		// Untracked empty directory hierarcy e/1 shall not conflict with merged
358 		// e/1
359 		FileUtils.mkdirs(new File(trash, "e/1"), true);
360 
361 		MergeResult mergeRes = git.merge().setStrategy(strategy)
362 				.include(masterCommit).call();
363 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
364 		assertEquals(
365 				"[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]",
366 				indexState(CONTENT));
367 	}
368 
369 	@Theory
370 	public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException,
371 			GitAPIException {
372 		Git git = Git.wrap(db);
373 		db.getConfig().setString("core", null, "autocrlf", "false");
374 		db.getConfig().save();
375 		writeTrashFile("crlf.txt", "some\r\ndata\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", "some\r\nmore\r\ndata\r\n");
382 		git.add().addFilepattern("crlf.txt").call();
383 		git.commit().setMessage("on master").call();
384 
385 		git.checkout().setName("brancha").call();
386 		writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n");
387 		git.add().addFilepattern("crlf.txt").call();
388 		git.commit().setMessage("on brancha").call();
389 
390 		db.getConfig().setString("core", null, "autocrlf", "input");
391 		db.getConfig().save();
392 
393 		MergeResult mergeResult = git.merge().setStrategy(strategy)
394 				.include(db.resolve("master"))
395 				.call();
396 		assertEquals(MergeResult.MergeStatus.MERGED,
397 				mergeResult.getMergeStatus());
398 	}
399 
400 	@Theory
401 	public void mergeWithCrlfAutoCrlfTrue(MergeStrategy strategy)
402 			throws IOException, GitAPIException {
403 		Git git = Git.wrap(db);
404 		db.getConfig().setString("core", null, "autocrlf", "true");
405 		db.getConfig().save();
406 		writeTrashFile("crlf.txt", "a crlf file\r\n");
407 		git.add().addFilepattern("crlf.txt").call();
408 		git.commit().setMessage("base").call();
409 
410 		git.branchCreate().setName("brancha").call();
411 
412 		writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
413 		git.add().addFilepattern("crlf.txt").call();
414 		git.commit().setMessage("on master").call();
415 
416 		git.checkout().setName("brancha").call();
417 		File testFile = writeTrashFile("crlf.txt",
418 				"a first line\r\na crlf file\r\n");
419 		git.add().addFilepattern("crlf.txt").call();
420 		git.commit().setMessage("on brancha").call();
421 
422 		MergeResult mergeResult = git.merge().setStrategy(strategy)
423 				.include(db.resolve("master")).call();
424 		assertEquals(MergeResult.MergeStatus.MERGED,
425 				mergeResult.getMergeStatus());
426 		checkFile(testFile, "a first line\r\na crlf file\r\na second line\r\n");
427 		assertEquals(
428 				"[crlf.txt, mode:100644, content:a first line\na crlf file\na second line\n]",
429 				indexState(CONTENT));
430 	}
431 
432 	@Theory
433 	public void rebaseWithCrlfAutoCrlfTrue(MergeStrategy strategy)
434 			throws IOException, GitAPIException {
435 		Git git = Git.wrap(db);
436 		db.getConfig().setString("core", null, "autocrlf", "true");
437 		db.getConfig().save();
438 		writeTrashFile("crlf.txt", "line 1\r\nline 2\r\nline 3\r\n");
439 		git.add().addFilepattern("crlf.txt").call();
440 		RevCommit first = git.commit().setMessage("base").call();
441 
442 		git.checkout().setCreateBranch(true).setStartPoint(first)
443 				.setName("brancha").call();
444 
445 		File testFile = writeTrashFile("crlf.txt",
446 				"line 1\r\nmodified line\r\nline 3\r\n");
447 		git.add().addFilepattern("crlf.txt").call();
448 		git.commit().setMessage("on brancha").call();
449 
450 		git.checkout().setName("master").call();
451 		File otherFile = writeTrashFile("otherfile.txt", "a line\r\n");
452 		git.add().addFilepattern("otherfile.txt").call();
453 		git.commit().setMessage("on master").call();
454 
455 		git.checkout().setName("brancha").call();
456 		checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
457 		assertFalse(otherFile.exists());
458 
459 		RebaseResult rebaseResult = git.rebase().setStrategy(strategy)
460 				.setUpstream(db.resolve("master")).call();
461 		assertEquals(RebaseResult.Status.OK, rebaseResult.getStatus());
462 		checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
463 		checkFile(otherFile, "a line\r\n");
464 		assertEquals(
465 				"[crlf.txt, mode:100644, content:line 1\nmodified line\nline 3\n]"
466 						+ "[otherfile.txt, mode:100644, content:a line\n]",
467 				indexState(CONTENT));
468 	}
469 
470 	/**
471 	 * Merging two equal subtrees when the index does not contain any file in
472 	 * that subtree should lead to a merged state.
473 	 *
474 	 * @param strategy
475 	 * @throws Exception
476 	 */
477 	@Theory
478 	public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
479 			throws Exception {
480 		Git git = Git.wrap(db);
481 
482 		writeTrashFile("d/1", "orig");
483 		git.add().addFilepattern("d/1").call();
484 		RevCommit first = git.commit().setMessage("added d/1").call();
485 
486 		writeTrashFile("d/1", "modified");
487 		RevCommit masterCommit = git.commit().setAll(true)
488 				.setMessage("modified d/1 on master").call();
489 
490 		git.checkout().setCreateBranch(true).setStartPoint(first)
491 				.setName("side").call();
492 		writeTrashFile("d/1", "modified");
493 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
494 
495 		git.rm().addFilepattern("d/1").call();
496 		git.rm().addFilepattern("d").call();
497 		MergeResult mergeRes = git.merge().setStrategy(strategy)
498 				.include(masterCommit).call();
499 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
500 		assertEquals("[d/1, mode:100644, content:modified]",
501 				indexState(CONTENT));
502 	}
503 
504 	/**
505 	 * Merging two equal subtrees with an incore merger should lead to a merged
506 	 * state.
507 	 *
508 	 * @param strategy
509 	 * @throws Exception
510 	 */
511 	@Theory
512 	public void checkMergeEqualTreesInCore(MergeStrategy strategy)
513 			throws Exception {
514 		Git git = Git.wrap(db);
515 
516 		writeTrashFile("d/1", "orig");
517 		git.add().addFilepattern("d/1").call();
518 		RevCommit first = git.commit().setMessage("added d/1").call();
519 
520 		writeTrashFile("d/1", "modified");
521 		RevCommit masterCommit = git.commit().setAll(true)
522 				.setMessage("modified d/1 on master").call();
523 
524 		git.checkout().setCreateBranch(true).setStartPoint(first)
525 				.setName("side").call();
526 		writeTrashFile("d/1", "modified");
527 		RevCommit sideCommit = git.commit().setAll(true)
528 				.setMessage("modified d/1 on side").call();
529 
530 		git.rm().addFilepattern("d/1").call();
531 		git.rm().addFilepattern("d").call();
532 
533 		ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
534 				true);
535 		boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
536 		assertTrue(noProblems);
537 	}
538 
539 	/**
540 	 * Merging two equal subtrees with an incore merger should lead to a merged
541 	 * state, without using a Repository (the 'Gerrit' use case).
542 	 *
543 	 * @param strategy
544 	 * @throws Exception
545 	 */
546 	@Theory
547 	public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy)
548 			throws Exception {
549 		Git git = Git.wrap(db);
550 
551 		writeTrashFile("d/1", "orig");
552 		git.add().addFilepattern("d/1").call();
553 		RevCommit first = git.commit().setMessage("added d/1").call();
554 
555 		writeTrashFile("d/1", "modified");
556 		RevCommit masterCommit = git.commit().setAll(true)
557 				.setMessage("modified d/1 on master").call();
558 
559 		git.checkout().setCreateBranch(true).setStartPoint(first)
560 				.setName("side").call();
561 		writeTrashFile("d/1", "modified");
562 		RevCommit sideCommit = git.commit().setAll(true)
563 				.setMessage("modified d/1 on side").call();
564 
565 		git.rm().addFilepattern("d/1").call();
566 		git.rm().addFilepattern("d").call();
567 
568 		try (ObjectInserter ins = db.newObjectInserter()) {
569 			ThreeWayMerger resolveMerger =
570 					(ThreeWayMerger) strategy.newMerger(ins, db.getConfig());
571 			boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
572 			assertTrue(noProblems);
573 		}
574 	}
575 
576 	/**
577 	 * Merging two equal subtrees when the index and HEAD does not contain any
578 	 * file in that subtree should lead to a merged state.
579 	 *
580 	 * @param strategy
581 	 * @throws Exception
582 	 */
583 	@Theory
584 	public void checkMergeEqualNewTrees(MergeStrategy strategy)
585 			throws Exception {
586 		Git git = Git.wrap(db);
587 
588 		writeTrashFile("2", "orig");
589 		git.add().addFilepattern("2").call();
590 		RevCommit first = git.commit().setMessage("added 2").call();
591 
592 		writeTrashFile("d/1", "orig");
593 		git.add().addFilepattern("d/1").call();
594 		RevCommit masterCommit = git.commit().setAll(true)
595 				.setMessage("added d/1 on master").call();
596 
597 		git.checkout().setCreateBranch(true).setStartPoint(first)
598 				.setName("side").call();
599 		writeTrashFile("d/1", "orig");
600 		git.add().addFilepattern("d/1").call();
601 		git.commit().setAll(true).setMessage("added d/1 on side").call();
602 
603 		git.rm().addFilepattern("d/1").call();
604 		git.rm().addFilepattern("d").call();
605 		MergeResult mergeRes = git.merge().setStrategy(strategy)
606 				.include(masterCommit).call();
607 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
608 		assertEquals(
609 				"[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
610 				indexState(CONTENT));
611 	}
612 
613 	/**
614 	 * Merging two conflicting subtrees when the index and HEAD does not contain
615 	 * any file in that subtree should lead to a conflicting state.
616 	 *
617 	 * @param strategy
618 	 * @throws Exception
619 	 */
620 	@Theory
621 	public void checkMergeConflictingNewTrees(MergeStrategy strategy)
622 			throws Exception {
623 		Git git = Git.wrap(db);
624 
625 		writeTrashFile("2", "orig");
626 		git.add().addFilepattern("2").call();
627 		RevCommit first = git.commit().setMessage("added 2").call();
628 
629 		writeTrashFile("d/1", "master");
630 		git.add().addFilepattern("d/1").call();
631 		RevCommit masterCommit = git.commit().setAll(true)
632 				.setMessage("added d/1 on master").call();
633 
634 		git.checkout().setCreateBranch(true).setStartPoint(first)
635 				.setName("side").call();
636 		writeTrashFile("d/1", "side");
637 		git.add().addFilepattern("d/1").call();
638 		git.commit().setAll(true).setMessage("added d/1 on side").call();
639 
640 		git.rm().addFilepattern("d/1").call();
641 		git.rm().addFilepattern("d").call();
642 		MergeResult mergeRes = git.merge().setStrategy(strategy)
643 				.include(masterCommit).call();
644 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
645 		assertEquals(
646 				"[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
647 				indexState(CONTENT));
648 	}
649 
650 	/**
651 	 * Merging two conflicting files when the index contains a tree for that
652 	 * path should lead to a failed state.
653 	 *
654 	 * @param strategy
655 	 * @throws Exception
656 	 */
657 	@Theory
658 	public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
659 			throws Exception {
660 		Git git = Git.wrap(db);
661 
662 		writeTrashFile("0", "orig");
663 		git.add().addFilepattern("0").call();
664 		RevCommit first = git.commit().setMessage("added 0").call();
665 
666 		writeTrashFile("0", "master");
667 		RevCommit masterCommit = git.commit().setAll(true)
668 				.setMessage("modified 0 on master").call();
669 
670 		git.checkout().setCreateBranch(true).setStartPoint(first)
671 				.setName("side").call();
672 		writeTrashFile("0", "side");
673 		git.commit().setAll(true).setMessage("modified 0 on side").call();
674 
675 		git.rm().addFilepattern("0").call();
676 		writeTrashFile("0/0", "side");
677 		git.add().addFilepattern("0/0").call();
678 		MergeResult mergeRes = git.merge().setStrategy(strategy)
679 				.include(masterCommit).call();
680 		assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
681 	}
682 
683 	/**
684 	 * Merging two equal files when the index contains a tree for that path
685 	 * should lead to a failed state.
686 	 *
687 	 * @param strategy
688 	 * @throws Exception
689 	 */
690 	@Theory
691 	public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
692 			throws Exception {
693 		Git git = Git.wrap(db);
694 
695 		writeTrashFile("0", "orig");
696 		writeTrashFile("1", "1\n2\n3");
697 		git.add().addFilepattern("0").addFilepattern("1").call();
698 		RevCommit first = git.commit().setMessage("added 0, 1").call();
699 
700 		writeTrashFile("1", "1master\n2\n3");
701 		RevCommit masterCommit = git.commit().setAll(true)
702 				.setMessage("modified 1 on master").call();
703 
704 		git.checkout().setCreateBranch(true).setStartPoint(first)
705 				.setName("side").call();
706 		writeTrashFile("1", "1\n2\n3side");
707 		git.commit().setAll(true).setMessage("modified 1 on side").call();
708 
709 		git.rm().addFilepattern("0").call();
710 		writeTrashFile("0/0", "modified");
711 		git.add().addFilepattern("0/0").call();
712 		try {
713 			git.merge().setStrategy(strategy).include(masterCommit).call();
714 			Assert.fail("Didn't get the expected exception");
715 		} catch (CheckoutConflictException e) {
716 			assertEquals(1, e.getConflictingPaths().size());
717 			assertEquals("0/0", e.getConflictingPaths().get(0));
718 		}
719 	}
720 
721 	@Theory
722 	public void checkContentMergeNoConflict(MergeStrategy strategy)
723 			throws Exception {
724 		Git git = Git.wrap(db);
725 
726 		writeTrashFile("file", "1\n2\n3");
727 		git.add().addFilepattern("file").call();
728 		RevCommit first = git.commit().setMessage("added file").call();
729 
730 		writeTrashFile("file", "1master\n2\n3");
731 		git.commit().setAll(true).setMessage("modified file on master").call();
732 
733 		git.checkout().setCreateBranch(true).setStartPoint(first)
734 				.setName("side").call();
735 		writeTrashFile("file", "1\n2\n3side");
736 		RevCommit sideCommit = git.commit().setAll(true)
737 				.setMessage("modified file on side").call();
738 
739 		git.checkout().setName("master").call();
740 		MergeResult result =
741 				git.merge().setStrategy(strategy).include(sideCommit).call();
742 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
743 		String expected = "1master\n2\n3side";
744 		assertEquals(expected, read("file"));
745 	}
746 
747 	@Theory
748 	public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy)
749 			throws Exception {
750 		Git git = Git.wrap(db);
751 
752 		writeTrashFile("file", "1\n2\n3");
753 		git.add().addFilepattern("file").call();
754 		RevCommit first = git.commit().setMessage("added file").call();
755 
756 		writeTrashFile("file", "1master\n2\n3");
757 		RevCommit masterCommit = git.commit().setAll(true)
758 				.setMessage("modified file on master").call();
759 
760 		git.checkout().setCreateBranch(true).setStartPoint(first)
761 				.setName("side").call();
762 		writeTrashFile("file", "1\n2\n3side");
763 		RevCommit sideCommit = git.commit().setAll(true)
764 				.setMessage("modified file on side").call();
765 
766 		try (ObjectInserter ins = db.newObjectInserter()) {
767 			ResolveMerger merger =
768 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
769 			boolean noProblems = merger.merge(masterCommit, sideCommit);
770 			assertTrue(noProblems);
771 			assertEquals("1master\n2\n3side",
772 					readBlob(merger.getResultTreeId(), "file"));
773 		}
774 	}
775 
776 
777 	/**
778 	 * Merging a change involving large binary files should short-circuit reads.
779 	 *
780 	 * @param strategy
781 	 * @throws Exception
782 	 */
783 	@Theory
784 	public void checkContentMergeLargeBinaries(MergeStrategy strategy) throws Exception {
785 		Git git = Git.wrap(db);
786 		final int LINELEN = 72;
787 
788 		// setup a merge that would work correctly if we disconsider the stray '\0'
789 		// that the file contains near the start.
790 		byte[] binary = new byte[LINELEN * 2000];
791 		for (int i = 0; i < binary.length; i++) {
792 			binary[i] = (byte)((i % LINELEN) == 0 ? '\n' : 'x');
793 		}
794 		binary[50] = '\0';
795 
796 		writeTrashFile("file", new String(binary, UTF_8));
797 		git.add().addFilepattern("file").call();
798 		RevCommit first = git.commit().setMessage("added file").call();
799 
800 		// Generate an edit in a single line.
801 		int idx = LINELEN * 1200 + 1;
802 		byte save = binary[idx];
803 		binary[idx] = '@';
804 		writeTrashFile("file", new String(binary, UTF_8));
805 
806 		binary[idx] = save;
807 		git.add().addFilepattern("file").call();
808 		RevCommit masterCommit = git.commit().setAll(true)
809 			.setMessage("modified file l 1200").call();
810 
811 		git.checkout().setCreateBranch(true).setStartPoint(first).setName("side").call();
812 		binary[LINELEN * 1500 + 1] = '!';
813 		writeTrashFile("file", new String(binary, UTF_8));
814 		git.add().addFilepattern("file").call();
815 		RevCommit sideCommit = git.commit().setAll(true)
816 			.setMessage("modified file l 1500").call();
817 
818 		try (ObjectInserter ins = db.newObjectInserter()) {
819 			// Check that we don't read the large blobs.
820 			ObjectInserter forbidInserter = new ObjectInserter.Filter() {
821 				@Override
822 				protected ObjectInserter delegate() {
823 					return ins;
824 				}
825 
826 				@Override
827 				public ObjectReader newReader() {
828 					return new BigReadForbiddenReader(super.newReader(), 8000);
829 				}
830 			};
831 
832 			ResolveMerger merger =
833 				(ResolveMerger) strategy.newMerger(forbidInserter, db.getConfig());
834 			boolean noProblems = merger.merge(masterCommit, sideCommit);
835 			assertFalse(noProblems);
836 		}
837 	}
838 
839 	/**
840 	 * Throws an exception if reading beyond limit.
841 	 */
842 	class BigReadForbiddenStream extends ObjectStream.Filter {
843 		int limit;
844 
845 		BigReadForbiddenStream(ObjectStream orig, int limit) {
846 			super(orig.getType(), orig.getSize(), orig);
847 			this.limit = limit;
848 		}
849 
850 		@Override
851 		public long skip(long n) throws IOException {
852 			limit -= n;
853 			if (limit < 0) {
854 				throw new IllegalStateException();
855 			}
856 
857 			return super.skip(n);
858 		}
859 
860 		@Override
861 		public int read() throws IOException {
862 			int r = super.read();
863 			limit--;
864 			if (limit < 0) {
865 				throw new IllegalStateException();
866 			}
867 			return r;
868 		}
869 
870 		@Override
871 		public int read(byte[] b, int off, int len) throws IOException {
872 			int n = super.read(b, off, len);
873 			limit -= n;
874 			if (limit < 0) {
875 				throw new IllegalStateException();
876 			}
877 			return n;
878 		}
879 	}
880 
881 	class BigReadForbiddenReader extends ObjectReader.Filter {
882 		ObjectReader delegate;
883 		int limit;
884 
885 		@Override
886 		protected ObjectReader delegate() {
887 			return delegate;
888 		}
889 
890 		BigReadForbiddenReader(ObjectReader delegate, int limit) {
891 			this.delegate = delegate;
892 			this.limit = limit;
893 		}
894 
895 		@Override
896 		public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
897 			ObjectLoader orig = super.open(objectId, typeHint);
898 			return new ObjectLoader.Filter() {
899 				@Override
900 				protected ObjectLoader delegate() {
901 					return orig;
902 				}
903 
904 				@Override
905 				public ObjectStream openStream() throws IOException {
906 					ObjectStream os = orig.openStream();
907 					return new BigReadForbiddenStream(os, limit);
908 				}
909 			};
910 		}
911 	}
912 
913 	@Theory
914 	public void checkContentMergeConflict(MergeStrategy strategy)
915 			throws Exception {
916 		Git git = Git.wrap(db);
917 
918 		writeTrashFile("file", "1\n2\n3");
919 		git.add().addFilepattern("file").call();
920 		RevCommit first = git.commit().setMessage("added file").call();
921 
922 		writeTrashFile("file", "1master\n2\n3");
923 		git.commit().setAll(true).setMessage("modified file on master").call();
924 
925 		git.checkout().setCreateBranch(true).setStartPoint(first)
926 				.setName("side").call();
927 		writeTrashFile("file", "1side\n2\n3");
928 		RevCommit sideCommit = git.commit().setAll(true)
929 				.setMessage("modified file on side").call();
930 
931 		git.checkout().setName("master").call();
932 		MergeResult result =
933 				git.merge().setStrategy(strategy).include(sideCommit).call();
934 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
935 		String expected = "<<<<<<< HEAD\n"
936 				+ "1master\n"
937 				+ "=======\n"
938 				+ "1side\n"
939 				+ ">>>>>>> " + sideCommit.name() + "\n"
940 				+ "2\n"
941 				+ "3";
942 		assertEquals(expected, read("file"));
943 	}
944 
945 	@Theory
946 	public void checkContentMergeConflict_noTree(MergeStrategy strategy)
947 			throws Exception {
948 		Git git = Git.wrap(db);
949 
950 		writeTrashFile("file", "1\n2\n3");
951 		git.add().addFilepattern("file").call();
952 		RevCommit first = git.commit().setMessage("added file").call();
953 
954 		writeTrashFile("file", "1master\n2\n3");
955 		RevCommit masterCommit = git.commit().setAll(true)
956 				.setMessage("modified file on master").call();
957 
958 		git.checkout().setCreateBranch(true).setStartPoint(first)
959 				.setName("side").call();
960 		writeTrashFile("file", "1side\n2\n3");
961 		RevCommit sideCommit = git.commit().setAll(true)
962 				.setMessage("modified file on side").call();
963 
964 		try (ObjectInserter ins = db.newObjectInserter()) {
965 			ResolveMerger merger =
966 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
967 			boolean noProblems = merger.merge(masterCommit, sideCommit);
968 			assertFalse(noProblems);
969 			assertEquals(Arrays.asList("file"), merger.getUnmergedPaths());
970 
971 			MergeFormatter fmt = new MergeFormatter();
972 			merger.getMergeResults().get("file");
973 			try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
974 				fmt.formatMerge(out, merger.getMergeResults().get("file"),
975 						"BASE", "OURS", "THEIRS", UTF_8.name());
976 				String expected = "<<<<<<< OURS\n"
977 						+ "1master\n"
978 						+ "=======\n"
979 						+ "1side\n"
980 						+ ">>>>>>> THEIRS\n"
981 						+ "2\n"
982 						+ "3";
983 				assertEquals(expected, new String(out.toByteArray(), UTF_8));
984 			}
985 		}
986 	}
987 
988 	/**
989 	 * Merging after criss-cross merges. In this case we merge together two
990 	 * commits which have two equally good common ancestors
991 	 *
992 	 * @param strategy
993 	 * @throws Exception
994 	 */
995 	@Theory
996 	public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
997 		Git git = Git.wrap(db);
998 
999 		writeTrashFile("1", "1\n2\n3");
1000 		git.add().addFilepattern("1").call();
1001 		RevCommit first = git.commit().setMessage("added 1").call();
1002 
1003 		writeTrashFile("1", "1master\n2\n3");
1004 		RevCommit masterCommit = git.commit().setAll(true)
1005 				.setMessage("modified 1 on master").call();
1006 
1007 		writeTrashFile("1", "1master2\n2\n3");
1008 		git.commit().setAll(true)
1009 				.setMessage("modified 1 on master again").call();
1010 
1011 		git.checkout().setCreateBranch(true).setStartPoint(first)
1012 				.setName("side").call();
1013 		writeTrashFile("1", "1\n2\na\nb\nc\n3side");
1014 		RevCommit sideCommit = git.commit().setAll(true)
1015 				.setMessage("modified 1 on side").call();
1016 
1017 		writeTrashFile("1", "1\n2\n3side2");
1018 		git.commit().setAll(true)
1019 				.setMessage("modified 1 on side again").call();
1020 
1021 		MergeResult result = git.merge().setStrategy(strategy)
1022 				.include(masterCommit).call();
1023 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
1024 		result.getNewHead();
1025 		git.checkout().setName("master").call();
1026 		result = git.merge().setStrategy(strategy).include(sideCommit).call();
1027 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
1028 
1029 		// we have two branches which are criss-cross merged. Try to merge the
1030 		// tips. This should succeed with RecursiveMerge and fail with
1031 		// ResolveMerge
1032 		try {
1033 			MergeResult mergeResult = git.merge().setStrategy(strategy)
1034 					.include(git.getRepository().exactRef("refs/heads/side"))
1035 					.call();
1036 			assertEquals(MergeStrategy.RECURSIVE, strategy);
1037 			assertEquals(MergeResult.MergeStatus.MERGED,
1038 					mergeResult.getMergeStatus());
1039 			assertEquals("1master2\n2\n3side2", read("1"));
1040 		} catch (JGitInternalException e) {
1041 			assertEquals(MergeStrategy.RESOLVE, strategy);
1042 			assertTrue(e.getCause() instanceof NoMergeBaseException);
1043 			assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
1044 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
1045 		}
1046 	}
1047 
1048 	@Theory
1049 	public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
1050 			throws Exception {
1051 		Git git = Git.wrap(db);
1052 
1053 		writeTrashFile("a.txt", "orig");
1054 		writeTrashFile("b.txt", "orig");
1055 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
1056 		RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
1057 
1058 		// modify and delete files on the master branch
1059 		writeTrashFile("a.txt", "master");
1060 		git.rm().addFilepattern("b.txt").call();
1061 		RevCommit masterCommit = git.commit()
1062 				.setMessage("modified a.txt, deleted b.txt").setAll(true)
1063 				.call();
1064 
1065 		// switch back to a side branch
1066 		git.checkout().setCreateBranch(true).setStartPoint(first)
1067 				.setName("side").call();
1068 		writeTrashFile("c.txt", "side");
1069 		git.add().addFilepattern("c.txt").call();
1070 		git.commit().setMessage("added c.txt").call();
1071 
1072 		// Get a handle to the the file so on windows it can't be deleted.
1073 		try (FileInputStream fis = new FileInputStream(
1074 				new File(db.getWorkTree(), "b.txt"))) {
1075 			MergeResult mergeRes = git.merge().setStrategy(strategy)
1076 					.include(masterCommit).call();
1077 			if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
1078 				// probably windows
1079 				assertEquals(1, mergeRes.getFailingPaths().size());
1080 				assertEquals(MergeFailureReason.COULD_NOT_DELETE,
1081 						mergeRes.getFailingPaths().get("b.txt"));
1082 			}
1083 			assertEquals(
1084 					"[a.txt, mode:100644, content:master]"
1085 							+ "[c.txt, mode:100644, content:side]",
1086 					indexState(CONTENT));
1087 		}
1088 	}
1089 
1090 	@Theory
1091 	public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
1092 		File f;
1093 		long lastTs4, lastTsIndex;
1094 		Git git = Git.wrap(db);
1095 		File indexFile = db.getIndexFile();
1096 
1097 		// Create initial content and remember when the last file was written.
1098 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1099 		lastTs4 = FS.DETECTED.lastModified(f);
1100 
1101 		// add all files, commit and check this doesn't update any working tree
1102 		// files and that the index is in a new file system timer tick. Make
1103 		// sure to wait long enough before adding so the index doesn't contain
1104 		// racily clean entries
1105 		fsTick(f);
1106 		git.add().addFilepattern(".").call();
1107 		RevCommit firstCommit = git.commit().setMessage("initial commit")
1108 				.call();
1109 		checkConsistentLastModified("0", "1", "2", "3", "4");
1110 		checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
1111 		assertEquals("Commit should not touch working tree file 4", lastTs4,
1112 				FS.DETECTED.lastModified(new File(db.getWorkTree(), "4")));
1113 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
1114 
1115 		// Do modifications on the master branch. Then add and commit. This
1116 		// should touch only "0", "2 and "3"
1117 		fsTick(indexFile);
1118 		f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
1119 				null);
1120 		fsTick(f);
1121 		git.add().addFilepattern(".").call();
1122 		RevCommit masterCommit = git.commit().setMessage("master commit")
1123 				.call();
1124 		checkConsistentLastModified("0", "1", "2", "3", "4");
1125 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1126 				+ lastTsIndex, "<0", "2", "3", "<.git/index");
1127 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
1128 
1129 		// Checkout a side branch. This should touch only "0", "2 and "3"
1130 		fsTick(indexFile);
1131 		git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
1132 				.setName("side").call();
1133 		checkConsistentLastModified("0", "1", "2", "3", "4");
1134 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
1135 				+ lastTsIndex, "<0", "2", "3", ".git/index");
1136 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
1137 
1138 		// This checkout may have populated worktree and index so fast that we
1139 		// may have smudged entries now. Check that we have the right content
1140 		// and then rewrite the index to get rid of smudged state
1141 		assertEquals("[0, mode:100644, content:orig]" //
1142 				+ "[1, mode:100644, content:orig]" //
1143 				+ "[2, mode:100644, content:1\n2\n3]" //
1144 				+ "[3, mode:100644, content:orig]" //
1145 				+ "[4, mode:100644, content:orig]", //
1146 				indexState(CONTENT));
1147 		fsTick(indexFile);
1148 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
1149 		lastTs4 = FS.DETECTED.lastModified(f);
1150 		fsTick(f);
1151 		git.add().addFilepattern(".").call();
1152 		checkConsistentLastModified("0", "1", "2", "3", "4");
1153 		checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
1154 				"4", "<.git/index");
1155 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
1156 
1157 		// Do modifications on the side branch. Touch only "1", "2 and "3"
1158 		fsTick(indexFile);
1159 		f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
1160 		fsTick(f);
1161 		git.add().addFilepattern(".").call();
1162 		git.commit().setMessage("side commit").call();
1163 		checkConsistentLastModified("0", "1", "2", "3", "4");
1164 		checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
1165 				+ lastTsIndex, "<1", "2", "3", "<.git/index");
1166 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
1167 
1168 		// merge master and side. Should only touch "0," "2" and "3"
1169 		fsTick(indexFile);
1170 		git.merge().setStrategy(strategy).include(masterCommit).call();
1171 		checkConsistentLastModified("0", "1", "2", "4");
1172 		checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
1173 				+ lastTsIndex, "<0", "2", "3", ".git/index");
1174 		assertEquals(
1175 				"[0, mode:100644, content:master]" //
1176 						+ "[1, mode:100644, content:side]" //
1177 						+ "[2, mode:100644, content:1master\n2\n3side]" //
1178 						+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
1179 						+ "[4, mode:100644, content:orig]", //
1180 				indexState(CONTENT));
1181 	}
1182 
1183 	/**
1184 	 * Merging two conflicting submodules when the index does not contain any
1185 	 * entry for that submodule.
1186 	 *
1187 	 * @param strategy
1188 	 * @throws Exception
1189 	 */
1190 	@Theory
1191 	public void checkMergeConflictingSubmodulesWithoutIndex(
1192 			MergeStrategy strategy) throws Exception {
1193 		Git git = Git.wrap(db);
1194 		writeTrashFile("initial", "initial");
1195 		git.add().addFilepattern("initial").call();
1196 		RevCommit initial = git.commit().setMessage("initial").call();
1197 
1198 		writeSubmodule("one", ObjectId
1199 				.fromString("1000000000000000000000000000000000000000"));
1200 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1201 		RevCommit right = git.commit().setMessage("added one").call();
1202 
1203 		// a second commit in the submodule
1204 
1205 		git.checkout().setStartPoint(initial).setName("left")
1206 				.setCreateBranch(true).call();
1207 		writeSubmodule("one", ObjectId
1208 				.fromString("2000000000000000000000000000000000000000"));
1209 
1210 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1211 		git.commit().setMessage("a different one").call();
1212 
1213 		MergeResult result = git.merge().setStrategy(strategy).include(right)
1214 				.call();
1215 
1216 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
1217 		Map<String, int[][]> conflicts = result.getConflicts();
1218 		assertEquals(1, conflicts.size());
1219 		assertNotNull(conflicts.get("one"));
1220 	}
1221 
1222 	/**
1223 	 * Merging two non-conflicting submodules when the index does not contain
1224 	 * any entry for either submodule.
1225 	 *
1226 	 * @param strategy
1227 	 * @throws Exception
1228 	 */
1229 	@Theory
1230 	public void checkMergeNonConflictingSubmodulesWithoutIndex(
1231 			MergeStrategy strategy) throws Exception {
1232 		Git git = Git.wrap(db);
1233 		writeTrashFile("initial", "initial");
1234 		git.add().addFilepattern("initial").call();
1235 
1236 		writeSubmodule("one", ObjectId
1237 				.fromString("1000000000000000000000000000000000000000"));
1238 
1239 		// Our initial commit should include a .gitmodules with a bunch of
1240 		// comment lines, so that
1241 		// we don't have a content merge issue when we add a new submodule at
1242 		// the top and a different
1243 		// one at the bottom. This is sort of a hack, but it should allow
1244 		// add/add submodule merges
1245 		String existing = read(Constants.DOT_GIT_MODULES);
1246 		String context = "\n# context\n# more context\n# yet more context\n";
1247 		write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1248 				existing + context + context + context);
1249 
1250 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1251 		RevCommit initial = git.commit().setMessage("initial").call();
1252 
1253 		writeSubmodule("two", ObjectId
1254 				.fromString("1000000000000000000000000000000000000000"));
1255 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1256 
1257 		RevCommit right = git.commit().setMessage("added two").call();
1258 
1259 		git.checkout().setStartPoint(initial).setName("left")
1260 				.setCreateBranch(true).call();
1261 
1262 		// we need to manually create the submodule for three for the
1263 		// .gitmodules hackery
1264 		addSubmoduleToIndex("three", ObjectId
1265 				.fromString("1000000000000000000000000000000000000000"));
1266 		new File(db.getWorkTree(), "three").mkdir();
1267 
1268 		existing = read(Constants.DOT_GIT_MODULES);
1269 		String three = "[submodule \"three\"]\n\tpath = three\n\turl = "
1270 				+ db.getDirectory().toURI() + "\n";
1271 		write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1272 				three + existing);
1273 
1274 		git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
1275 		git.commit().setMessage("a different one").call();
1276 
1277 		MergeResult result = git.merge().setStrategy(strategy).include(right)
1278 				.call();
1279 
1280 		assertNull(result.getCheckoutConflicts());
1281 		assertNull(result.getFailingPaths());
1282 		for (String dir : Arrays.asList("one", "two", "three")) {
1283 			assertTrue(new File(db.getWorkTree(), dir).isDirectory());
1284 		}
1285 	}
1286 
1287 	private void writeSubmodule(String path, ObjectId commit)
1288 			throws IOException, ConfigInvalidException {
1289 		addSubmoduleToIndex(path, commit);
1290 		new File(db.getWorkTree(), path).mkdir();
1291 
1292 		StoredConfig config = db.getConfig();
1293 		config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1294 				ConfigConstants.CONFIG_KEY_URL,
1295 				db.getDirectory().toURI().toString());
1296 		config.save();
1297 
1298 		FileBasedConfig modulesConfig = new FileBasedConfig(
1299 				new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
1300 				db.getFS());
1301 		modulesConfig.load();
1302 		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
1303 				ConfigConstants.CONFIG_KEY_PATH, path);
1304 		modulesConfig.save();
1305 
1306 	}
1307 
1308 	private void addSubmoduleToIndex(String path, ObjectId commit)
1309 			throws IOException {
1310 		DirCache cache = db.lockDirCache();
1311 		DirCacheEditor editor = cache.editor();
1312 		editor.add(new DirCacheEditor.PathEdit(path) {
1313 
1314 			@Override
1315 			public void apply(DirCacheEntry ent) {
1316 				ent.setFileMode(FileMode.GITLINK);
1317 				ent.setObjectId(commit);
1318 			}
1319 		});
1320 		editor.commit();
1321 	}
1322 
1323 	// Assert that every specified index entry has the same last modification
1324 	// timestamp as the associated file
1325 	private void checkConsistentLastModified(String... pathes)
1326 			throws IOException {
1327 		DirCache dc = db.readDirCache();
1328 		File workTree = db.getWorkTree();
1329 		for (String path : pathes)
1330 			assertEquals(
1331 					"IndexEntry with path "
1332 							+ path
1333 							+ " has lastmodified with is different from the worktree file",
1334 					FS.DETECTED.lastModified(new File(workTree, path)), dc.getEntry(path)
1335 							.getLastModified());
1336 	}
1337 
1338 	// Assert that modification timestamps of working tree files are as
1339 	// expected. You may specify n files. It is asserted that every file
1340 	// i+1 is not older than file i. If a path of file i+1 is prefixed with "<"
1341 	// then this file must be younger then file i. A path "*<modtime>"
1342 	// represents a file with a modification time of <modtime>
1343 	// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
1344 	private void checkModificationTimeStampOrder(String... pathes)
1345 			throws IOException {
1346 		long lastMod = Long.MIN_VALUE;
1347 		for (String p : pathes) {
1348 			boolean strong = p.startsWith("<");
1349 			boolean fixed = p.charAt(strong ? 1 : 0) == '*';
1350 			p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
1351 			long curMod = fixed ? Long.valueOf(p).longValue()
1352 					: FS.DETECTED.lastModified(new File(db.getWorkTree(), p));
1353 			if (strong)
1354 				assertTrue("path " + p + " is not younger than predecesssor",
1355 						curMod > lastMod);
1356 			else
1357 				assertTrue("path " + p + " is older than predecesssor",
1358 						curMod >= lastMod);
1359 		}
1360 	}
1361 
1362 	private String readBlob(ObjectId treeish, String path) throws Exception {
1363 		TestRepository<?> tr = new TestRepository<>(db);
1364 		RevWalk rw = tr.getRevWalk();
1365 		RevTree tree = rw.parseTree(treeish);
1366 		RevObject obj = tr.get(tree, path);
1367 		if (obj == null) {
1368 			return null;
1369 		}
1370 		return new String(rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(),
1371 				UTF_8);
1372 	}
1373 }