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 org.junit.Assert.assertEquals;
46  import static org.junit.Assert.assertTrue;
47  
48  import java.io.File;
49  import java.io.FileInputStream;
50  import java.io.IOException;
51  
52  import org.eclipse.jgit.api.Git;
53  import org.eclipse.jgit.api.MergeResult;
54  import org.eclipse.jgit.api.MergeResult.MergeStatus;
55  import org.eclipse.jgit.api.errors.CheckoutConflictException;
56  import org.eclipse.jgit.api.errors.GitAPIException;
57  import org.eclipse.jgit.api.errors.JGitInternalException;
58  import org.eclipse.jgit.dircache.DirCache;
59  import org.eclipse.jgit.errors.NoMergeBaseException;
60  import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
61  import org.eclipse.jgit.junit.RepositoryTestCase;
62  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
63  import org.eclipse.jgit.revwalk.RevCommit;
64  import org.eclipse.jgit.treewalk.FileTreeIterator;
65  import org.eclipse.jgit.util.FileUtils;
66  import org.junit.Assert;
67  import org.junit.experimental.theories.DataPoint;
68  import org.junit.experimental.theories.Theories;
69  import org.junit.experimental.theories.Theory;
70  import org.junit.runner.RunWith;
71  
72  @RunWith(Theories.class)
73  public class ResolveMergerTest extends RepositoryTestCase {
74  
75  	@DataPoint
76  	public static MergeStrategy resolve = MergeStrategy.RESOLVE;
77  
78  	@DataPoint
79  	public static MergeStrategy recursive = MergeStrategy.RECURSIVE;
80  
81  	@Theory
82  	public void failingDeleteOfDirectoryWithUntrackedContent(
83  			MergeStrategy strategy) throws Exception {
84  		File folder1 = new File(db.getWorkTree(), "folder1");
85  		FileUtils.mkdir(folder1);
86  		File file = new File(folder1, "file1.txt");
87  		write(file, "folder1--file1.txt");
88  		file = new File(folder1, "file2.txt");
89  		write(file, "folder1--file2.txt");
90  
91  		Git git = new Git(db);
92  		git.add().addFilepattern(folder1.getName()).call();
93  		RevCommit base = git.commit().setMessage("adding folder").call();
94  
95  		recursiveDelete(folder1);
96  		git.rm().addFilepattern("folder1/file1.txt")
97  				.addFilepattern("folder1/file2.txt").call();
98  		RevCommit other = git.commit()
99  				.setMessage("removing folders on 'other'").call();
100 
101 		git.checkout().setName(base.name()).call();
102 
103 		file = new File(db.getWorkTree(), "unrelated.txt");
104 		write(file, "unrelated");
105 
106 		git.add().addFilepattern("unrelated.txt").call();
107 		RevCommit head = git.commit().setMessage("Adding another file").call();
108 
109 		// Untracked file to cause failing path for delete() of folder1
110 		// but that's ok.
111 		file = new File(folder1, "file3.txt");
112 		write(file, "folder1--file3.txt");
113 
114 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
115 		merger.setCommitNames(new String[] { "BASE", "HEAD", "other" });
116 		merger.setWorkingTreeIterator(new FileTreeIterator(db));
117 		boolean ok = merger.merge(head.getId(), other.getId());
118 		assertTrue(ok);
119 		assertTrue(file.exists());
120 	}
121 
122 	/**
123 	 * Merging two conflicting subtrees when the index does not contain any file
124 	 * in that subtree should lead to a conflicting state.
125 	 *
126 	 * @param strategy
127 	 * @throws Exception
128 	 */
129 	@Theory
130 	public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy)
131 			throws Exception {
132 		Git git = Git.wrap(db);
133 
134 		writeTrashFile("d/1", "orig");
135 		git.add().addFilepattern("d/1").call();
136 		RevCommit first = git.commit().setMessage("added d/1").call();
137 
138 		writeTrashFile("d/1", "master");
139 		RevCommit masterCommit = git.commit().setAll(true)
140 				.setMessage("modified d/1 on master").call();
141 
142 		git.checkout().setCreateBranch(true).setStartPoint(first)
143 				.setName("side").call();
144 		writeTrashFile("d/1", "side");
145 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
146 
147 		git.rm().addFilepattern("d/1").call();
148 		git.rm().addFilepattern("d").call();
149 		MergeResult mergeRes = git.merge().setStrategy(strategy)
150 				.include(masterCommit).call();
151 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
152 		assertEquals(
153 				"[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
154 				indexState(CONTENT));
155 	}
156 
157 	/**
158 	 * Merging two different but mergeable subtrees when the index does not
159 	 * contain any file in that subtree should lead to a merged state.
160 	 *
161 	 * @param strategy
162 	 * @throws Exception
163 	 */
164 	@Theory
165 	public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy)
166 			throws Exception {
167 		Git git = Git.wrap(db);
168 
169 		writeTrashFile("d/1", "1\n2\n3");
170 		git.add().addFilepattern("d/1").call();
171 		RevCommit first = git.commit().setMessage("added d/1").call();
172 
173 		writeTrashFile("d/1", "1master\n2\n3");
174 		RevCommit masterCommit = git.commit().setAll(true)
175 				.setMessage("modified d/1 on master").call();
176 
177 		git.checkout().setCreateBranch(true).setStartPoint(first)
178 				.setName("side").call();
179 		writeTrashFile("d/1", "1\n2\n3side");
180 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
181 
182 		git.rm().addFilepattern("d/1").call();
183 		git.rm().addFilepattern("d").call();
184 		MergeResult mergeRes = git.merge().setStrategy(strategy)
185 				.include(masterCommit).call();
186 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
187 		assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
188 				indexState(CONTENT));
189 	}
190 
191 	/**
192 	 * An existing directory without tracked content should not prevent merging
193 	 * a tree where that directory exists.
194 	 *
195 	 * @param strategy
196 	 * @throws Exception
197 	 */
198 	@Theory
199 	public void checkUntrackedFolderIsNotAConflict(
200 			MergeStrategy strategy) throws Exception {
201 		Git git = Git.wrap(db);
202 
203 		writeTrashFile("d/1", "1");
204 		git.add().addFilepattern("d/1").call();
205 		RevCommit first = git.commit().setMessage("added d/1").call();
206 
207 		writeTrashFile("e/1", "4");
208 		git.add().addFilepattern("e/1").call();
209 		RevCommit masterCommit = git.commit().setMessage("added e/1").call();
210 
211 		git.checkout().setCreateBranch(true).setStartPoint(first)
212 				.setName("side").call();
213 		writeTrashFile("f/1", "5");
214 		git.add().addFilepattern("f/1").call();
215 		git.commit().setAll(true).setMessage("added f/1")
216 				.call();
217 
218 		// Untracked directory e shall not conflict with merged e/1
219 		writeTrashFile("e/2", "d two");
220 
221 		MergeResult mergeRes = git.merge().setStrategy(strategy)
222 				.include(masterCommit).call();
223 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
224 		assertEquals(
225 				"[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]",
226 				indexState(CONTENT));
227 	}
228 
229 	/**
230 	 * An existing directory without tracked content should not prevent merging
231 	 * a file with that name.
232 	 *
233 	 * @param strategy
234 	 * @throws Exception
235 	 */
236 	@Theory
237 	public void checkUntrackedEmpytFolderIsNotAConflictWithFile(
238 			MergeStrategy strategy)
239 			throws Exception {
240 		Git git = Git.wrap(db);
241 
242 		writeTrashFile("d/1", "1");
243 		git.add().addFilepattern("d/1").call();
244 		RevCommit first = git.commit().setMessage("added d/1").call();
245 
246 		writeTrashFile("e", "4");
247 		git.add().addFilepattern("e").call();
248 		RevCommit masterCommit = git.commit().setMessage("added e").call();
249 
250 		git.checkout().setCreateBranch(true).setStartPoint(first)
251 				.setName("side").call();
252 		writeTrashFile("f/1", "5");
253 		git.add().addFilepattern("f/1").call();
254 		git.commit().setAll(true).setMessage("added f/1").call();
255 
256 		// Untracked empty directory hierarcy e/1 shall not conflict with merged
257 		// e/1
258 		FileUtils.mkdirs(new File(trash, "e/1"), true);
259 
260 		MergeResult mergeRes = git.merge().setStrategy(strategy)
261 				.include(masterCommit).call();
262 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
263 		assertEquals(
264 				"[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]",
265 				indexState(CONTENT));
266 	}
267 
268 	@Theory
269 	public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException,
270 			GitAPIException {
271 		Git git = Git.wrap(db);
272 		db.getConfig().setString("core", null, "autocrlf", "false");
273 		db.getConfig().save();
274 		writeTrashFile("crlf.txt", "some\r\ndata\r\n");
275 		git.add().addFilepattern("crlf.txt").call();
276 		git.commit().setMessage("base").call();
277 
278 		git.branchCreate().setName("brancha").call();
279 
280 		writeTrashFile("crlf.txt", "some\r\nmore\r\ndata\r\n");
281 		git.add().addFilepattern("crlf.txt").call();
282 		git.commit().setMessage("on master").call();
283 
284 		git.checkout().setName("brancha").call();
285 		writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n");
286 		git.add().addFilepattern("crlf.txt").call();
287 		git.commit().setMessage("on brancha").call();
288 
289 		db.getConfig().setString("core", null, "autocrlf", "input");
290 		db.getConfig().save();
291 
292 		MergeResult mergeResult = git.merge().setStrategy(strategy)
293 				.include(db.resolve("master"))
294 				.call();
295 		assertEquals(MergeResult.MergeStatus.MERGED,
296 				mergeResult.getMergeStatus());
297 	}
298 
299 	/**
300 	 * Merging two equal subtrees when the index does not contain any file in
301 	 * that subtree should lead to a merged state.
302 	 *
303 	 * @param strategy
304 	 * @throws Exception
305 	 */
306 	@Theory
307 	public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
308 			throws Exception {
309 		Git git = Git.wrap(db);
310 
311 		writeTrashFile("d/1", "orig");
312 		git.add().addFilepattern("d/1").call();
313 		RevCommit first = git.commit().setMessage("added d/1").call();
314 
315 		writeTrashFile("d/1", "modified");
316 		RevCommit masterCommit = git.commit().setAll(true)
317 				.setMessage("modified d/1 on master").call();
318 
319 		git.checkout().setCreateBranch(true).setStartPoint(first)
320 				.setName("side").call();
321 		writeTrashFile("d/1", "modified");
322 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
323 
324 		git.rm().addFilepattern("d/1").call();
325 		git.rm().addFilepattern("d").call();
326 		MergeResult mergeRes = git.merge().setStrategy(strategy)
327 				.include(masterCommit).call();
328 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
329 		assertEquals("[d/1, mode:100644, content:modified]",
330 				indexState(CONTENT));
331 	}
332 
333 	/**
334 	 * Merging two equal subtrees with an incore merger should lead to a merged
335 	 * state (The 'Gerrit' use case).
336 	 *
337 	 * @param strategy
338 	 * @throws Exception
339 	 */
340 	@Theory
341 	public void checkMergeEqualTreesInCore(MergeStrategy strategy)
342 			throws Exception {
343 		Git git = Git.wrap(db);
344 
345 		writeTrashFile("d/1", "orig");
346 		git.add().addFilepattern("d/1").call();
347 		RevCommit first = git.commit().setMessage("added d/1").call();
348 
349 		writeTrashFile("d/1", "modified");
350 		RevCommit masterCommit = git.commit().setAll(true)
351 				.setMessage("modified d/1 on master").call();
352 
353 		git.checkout().setCreateBranch(true).setStartPoint(first)
354 				.setName("side").call();
355 		writeTrashFile("d/1", "modified");
356 		RevCommit sideCommit = git.commit().setAll(true)
357 				.setMessage("modified d/1 on side").call();
358 
359 		git.rm().addFilepattern("d/1").call();
360 		git.rm().addFilepattern("d").call();
361 
362 		ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
363 				true);
364 		boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
365 		assertTrue(noProblems);
366 	}
367 
368 	/**
369 	 * Merging two equal subtrees when the index and HEAD does not contain any
370 	 * file in that subtree should lead to a merged state.
371 	 *
372 	 * @param strategy
373 	 * @throws Exception
374 	 */
375 	@Theory
376 	public void checkMergeEqualNewTrees(MergeStrategy strategy)
377 			throws Exception {
378 		Git git = Git.wrap(db);
379 
380 		writeTrashFile("2", "orig");
381 		git.add().addFilepattern("2").call();
382 		RevCommit first = git.commit().setMessage("added 2").call();
383 
384 		writeTrashFile("d/1", "orig");
385 		git.add().addFilepattern("d/1").call();
386 		RevCommit masterCommit = git.commit().setAll(true)
387 				.setMessage("added d/1 on master").call();
388 
389 		git.checkout().setCreateBranch(true).setStartPoint(first)
390 				.setName("side").call();
391 		writeTrashFile("d/1", "orig");
392 		git.add().addFilepattern("d/1").call();
393 		git.commit().setAll(true).setMessage("added d/1 on side").call();
394 
395 		git.rm().addFilepattern("d/1").call();
396 		git.rm().addFilepattern("d").call();
397 		MergeResult mergeRes = git.merge().setStrategy(strategy)
398 				.include(masterCommit).call();
399 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
400 		assertEquals(
401 				"[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
402 				indexState(CONTENT));
403 	}
404 
405 	/**
406 	 * Merging two conflicting subtrees when the index and HEAD does not contain
407 	 * any file in that subtree should lead to a conflicting state.
408 	 *
409 	 * @param strategy
410 	 * @throws Exception
411 	 */
412 	@Theory
413 	public void checkMergeConflictingNewTrees(MergeStrategy strategy)
414 			throws Exception {
415 		Git git = Git.wrap(db);
416 
417 		writeTrashFile("2", "orig");
418 		git.add().addFilepattern("2").call();
419 		RevCommit first = git.commit().setMessage("added 2").call();
420 
421 		writeTrashFile("d/1", "master");
422 		git.add().addFilepattern("d/1").call();
423 		RevCommit masterCommit = git.commit().setAll(true)
424 				.setMessage("added d/1 on master").call();
425 
426 		git.checkout().setCreateBranch(true).setStartPoint(first)
427 				.setName("side").call();
428 		writeTrashFile("d/1", "side");
429 		git.add().addFilepattern("d/1").call();
430 		git.commit().setAll(true).setMessage("added d/1 on side").call();
431 
432 		git.rm().addFilepattern("d/1").call();
433 		git.rm().addFilepattern("d").call();
434 		MergeResult mergeRes = git.merge().setStrategy(strategy)
435 				.include(masterCommit).call();
436 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
437 		assertEquals(
438 				"[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
439 				indexState(CONTENT));
440 	}
441 
442 	/**
443 	 * Merging two conflicting files when the index contains a tree for that
444 	 * path should lead to a failed state.
445 	 *
446 	 * @param strategy
447 	 * @throws Exception
448 	 */
449 	@Theory
450 	public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
451 			throws Exception {
452 		Git git = Git.wrap(db);
453 
454 		writeTrashFile("0", "orig");
455 		git.add().addFilepattern("0").call();
456 		RevCommit first = git.commit().setMessage("added 0").call();
457 
458 		writeTrashFile("0", "master");
459 		RevCommit masterCommit = git.commit().setAll(true)
460 				.setMessage("modified 0 on master").call();
461 
462 		git.checkout().setCreateBranch(true).setStartPoint(first)
463 				.setName("side").call();
464 		writeTrashFile("0", "side");
465 		git.commit().setAll(true).setMessage("modified 0 on side").call();
466 
467 		git.rm().addFilepattern("0").call();
468 		writeTrashFile("0/0", "side");
469 		git.add().addFilepattern("0/0").call();
470 		MergeResult mergeRes = git.merge().setStrategy(strategy)
471 				.include(masterCommit).call();
472 		assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
473 	}
474 
475 	/**
476 	 * Merging two equal files when the index contains a tree for that path
477 	 * should lead to a failed state.
478 	 *
479 	 * @param strategy
480 	 * @throws Exception
481 	 */
482 	@Theory
483 	public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
484 			throws Exception {
485 		Git git = Git.wrap(db);
486 
487 		writeTrashFile("0", "orig");
488 		writeTrashFile("1", "1\n2\n3");
489 		git.add().addFilepattern("0").addFilepattern("1").call();
490 		RevCommit first = git.commit().setMessage("added 0, 1").call();
491 
492 		writeTrashFile("1", "1master\n2\n3");
493 		RevCommit masterCommit = git.commit().setAll(true)
494 				.setMessage("modified 1 on master").call();
495 
496 		git.checkout().setCreateBranch(true).setStartPoint(first)
497 				.setName("side").call();
498 		writeTrashFile("1", "1\n2\n3side");
499 		git.commit().setAll(true).setMessage("modified 1 on side").call();
500 
501 		git.rm().addFilepattern("0").call();
502 		writeTrashFile("0/0", "modified");
503 		git.add().addFilepattern("0/0").call();
504 		try {
505 			git.merge().setStrategy(strategy).include(masterCommit).call();
506 			Assert.fail("Didn't get the expected exception");
507 		} catch (CheckoutConflictException e) {
508 			assertEquals(1, e.getConflictingPaths().size());
509 			assertEquals("0/0", e.getConflictingPaths().get(0));
510 		}
511 	}
512 
513 	/**
514 	 * Merging after criss-cross merges. In this case we merge together two
515 	 * commits which have two equally good common ancestors
516 	 *
517 	 * @param strategy
518 	 * @throws Exception
519 	 */
520 	@Theory
521 	public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
522 		Git git = Git.wrap(db);
523 
524 		writeTrashFile("1", "1\n2\n3");
525 		git.add().addFilepattern("1").call();
526 		RevCommit first = git.commit().setMessage("added 1").call();
527 
528 		writeTrashFile("1", "1master\n2\n3");
529 		RevCommit masterCommit = git.commit().setAll(true)
530 				.setMessage("modified 1 on master").call();
531 
532 		writeTrashFile("1", "1master2\n2\n3");
533 		git.commit().setAll(true)
534 				.setMessage("modified 1 on master again").call();
535 
536 		git.checkout().setCreateBranch(true).setStartPoint(first)
537 				.setName("side").call();
538 		writeTrashFile("1", "1\n2\na\nb\nc\n3side");
539 		RevCommit sideCommit = git.commit().setAll(true)
540 				.setMessage("modified 1 on side").call();
541 
542 		writeTrashFile("1", "1\n2\n3side2");
543 		git.commit().setAll(true)
544 				.setMessage("modified 1 on side again").call();
545 
546 		MergeResult result = git.merge().setStrategy(strategy)
547 				.include(masterCommit).call();
548 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
549 		result.getNewHead();
550 		git.checkout().setName("master").call();
551 		result = git.merge().setStrategy(strategy).include(sideCommit).call();
552 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
553 
554 		// we have two branches which are criss-cross merged. Try to merge the
555 		// tips. This should succeed with RecursiveMerge and fail with
556 		// ResolveMerge
557 		try {
558 			MergeResult mergeResult = git.merge().setStrategy(strategy)
559 					.include(git.getRepository().getRef("refs/heads/side"))
560 					.call();
561 			assertEquals(MergeStrategy.RECURSIVE, strategy);
562 			assertEquals(MergeResult.MergeStatus.MERGED,
563 					mergeResult.getMergeStatus());
564 			assertEquals("1master2\n2\n3side2", read("1"));
565 		} catch (JGitInternalException e) {
566 			assertEquals(MergeStrategy.RESOLVE, strategy);
567 			assertTrue(e.getCause() instanceof NoMergeBaseException);
568 			assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
569 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
570 		}
571 	}
572 
573 	@Theory
574 	public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
575 			throws Exception {
576 		Git git = Git.wrap(db);
577 
578 		writeTrashFile("a.txt", "orig");
579 		writeTrashFile("b.txt", "orig");
580 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
581 		RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
582 
583 		// modify and delete files on the master branch
584 		writeTrashFile("a.txt", "master");
585 		git.rm().addFilepattern("b.txt").call();
586 		RevCommit masterCommit = git.commit()
587 				.setMessage("modified a.txt, deleted b.txt").setAll(true)
588 				.call();
589 
590 		// switch back to a side branch
591 		git.checkout().setCreateBranch(true).setStartPoint(first)
592 				.setName("side").call();
593 		writeTrashFile("c.txt", "side");
594 		git.add().addFilepattern("c.txt").call();
595 		git.commit().setMessage("added c.txt").call();
596 
597 		// Get a handle to the the file so on windows it can't be deleted.
598 		FileInputStream fis = new FileInputStream(new File(db.getWorkTree(),
599 				"b.txt"));
600 		MergeResult mergeRes = git.merge().setStrategy(strategy)
601 				.include(masterCommit).call();
602 		if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
603 			// probably windows
604 			assertEquals(1, mergeRes.getFailingPaths().size());
605 			assertEquals(MergeFailureReason.COULD_NOT_DELETE, mergeRes
606 					.getFailingPaths().get("b.txt"));
607 		}
608 		assertEquals("[a.txt, mode:100644, content:master]"
609 				+ "[c.txt, mode:100644, content:side]", indexState(CONTENT));
610 		fis.close();
611 	}
612 
613 	@Theory
614 	public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
615 		File f;
616 		long lastTs4, lastTsIndex;
617 		Git git = Git.wrap(db);
618 		File indexFile = db.getIndexFile();
619 
620 		// Create initial content and remember when the last file was written.
621 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
622 		lastTs4 = f.lastModified();
623 
624 		// add all files, commit and check this doesn't update any working tree
625 		// files and that the index is in a new file system timer tick. Make
626 		// sure to wait long enough before adding so the index doesn't contain
627 		// racily clean entries
628 		fsTick(f);
629 		git.add().addFilepattern(".").call();
630 		RevCommit firstCommit = git.commit().setMessage("initial commit")
631 				.call();
632 		checkConsistentLastModified("0", "1", "2", "3", "4");
633 		checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
634 		assertEquals("Commit should not touch working tree file 4", lastTs4,
635 				new File(db.getWorkTree(), "4").lastModified());
636 		lastTsIndex = indexFile.lastModified();
637 
638 		// Do modifications on the master branch. Then add and commit. This
639 		// should touch only "0", "2 and "3"
640 		fsTick(indexFile);
641 		f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
642 				null);
643 		fsTick(f);
644 		git.add().addFilepattern(".").call();
645 		RevCommit masterCommit = git.commit().setMessage("master commit")
646 				.call();
647 		checkConsistentLastModified("0", "1", "2", "3", "4");
648 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
649 				+ lastTsIndex, "<0", "2", "3", "<.git/index");
650 		lastTsIndex = indexFile.lastModified();
651 
652 		// Checkout a side branch. This should touch only "0", "2 and "3"
653 		fsTick(indexFile);
654 		git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
655 				.setName("side").call();
656 		checkConsistentLastModified("0", "1", "2", "3", "4");
657 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
658 				+ lastTsIndex, "<0", "2", "3", ".git/index");
659 		lastTsIndex = indexFile.lastModified();
660 
661 		// This checkout may have populated worktree and index so fast that we
662 		// may have smudged entries now. Check that we have the right content
663 		// and then rewrite the index to get rid of smudged state
664 		assertEquals("[0, mode:100644, content:orig]" //
665 				+ "[1, mode:100644, content:orig]" //
666 				+ "[2, mode:100644, content:1\n2\n3]" //
667 				+ "[3, mode:100644, content:orig]" //
668 				+ "[4, mode:100644, content:orig]", //
669 				indexState(CONTENT));
670 		fsTick(indexFile);
671 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
672 		lastTs4 = f.lastModified();
673 		fsTick(f);
674 		git.add().addFilepattern(".").call();
675 		checkConsistentLastModified("0", "1", "2", "3", "4");
676 		checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
677 				"4", "<.git/index");
678 		lastTsIndex = indexFile.lastModified();
679 
680 		// Do modifications on the side branch. Touch only "1", "2 and "3"
681 		fsTick(indexFile);
682 		f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
683 		fsTick(f);
684 		git.add().addFilepattern(".").call();
685 		git.commit().setMessage("side commit").call();
686 		checkConsistentLastModified("0", "1", "2", "3", "4");
687 		checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
688 				+ lastTsIndex, "<1", "2", "3", "<.git/index");
689 		lastTsIndex = indexFile.lastModified();
690 
691 		// merge master and side. Should only touch "0," "2" and "3"
692 		fsTick(indexFile);
693 		git.merge().setStrategy(strategy).include(masterCommit).call();
694 		checkConsistentLastModified("0", "1", "2", "4");
695 		checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
696 				+ lastTsIndex, "<0", "2", "3", ".git/index");
697 		assertEquals(
698 				"[0, mode:100644, content:master]" //
699 						+ "[1, mode:100644, content:side]" //
700 						+ "[2, mode:100644, content:1master\n2\n3side]" //
701 						+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
702 						+ "[4, mode:100644, content:orig]", //
703 				indexState(CONTENT));
704 	}
705 
706 	// Assert that every specified index entry has the same last modification
707 	// timestamp as the associated file
708 	private void checkConsistentLastModified(String... pathes)
709 			throws IOException {
710 		DirCache dc = db.readDirCache();
711 		File workTree = db.getWorkTree();
712 		for (String path : pathes)
713 			assertEquals(
714 					"IndexEntry with path "
715 							+ path
716 							+ " has lastmodified with is different from the worktree file",
717 					new File(workTree, path).lastModified(), dc.getEntry(path)
718 							.getLastModified());
719 	}
720 
721 	// Assert that modification timestamps of working tree files are as
722 	// expected. You may specify n files. It is asserted that every file
723 	// i+1 is not older than file i. If a path of file i+1 is prefixed with "<"
724 	// then this file must be younger then file i. A path "*<modtime>"
725 	// represents a file with a modification time of <modtime>
726 	// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
727 	private void checkModificationTimeStampOrder(String... pathes) {
728 		long lastMod = Long.MIN_VALUE;
729 		for (String p : pathes) {
730 			boolean strong = p.startsWith("<");
731 			boolean fixed = p.charAt(strong ? 1 : 0) == '*';
732 			p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
733 			long curMod = fixed ? Long.valueOf(p).longValue() : new File(
734 					db.getWorkTree(), p).lastModified();
735 			if (strong)
736 				assertTrue("path " + p + " is not younger than predecesssor",
737 						curMod > lastMod);
738 			else
739 				assertTrue("path " + p + " is older than predecesssor",
740 						curMod >= lastMod);
741 		}
742 	}
743 }