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