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