View Javadoc
1   /*
2    * Copyright (C) 2012, Robin Stocker <robin@nibor.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.merge;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
47  import static org.junit.Assert.assertEquals;
48  import static org.junit.Assert.assertFalse;
49  import static org.junit.Assert.assertTrue;
50  
51  import java.io.ByteArrayOutputStream;
52  import java.io.File;
53  import java.io.FileInputStream;
54  import java.io.IOException;
55  import java.util.Arrays;
56  
57  import org.eclipse.jgit.api.Git;
58  import org.eclipse.jgit.api.MergeResult;
59  import org.eclipse.jgit.api.MergeResult.MergeStatus;
60  import org.eclipse.jgit.api.errors.CheckoutConflictException;
61  import org.eclipse.jgit.api.errors.GitAPIException;
62  import org.eclipse.jgit.api.errors.JGitInternalException;
63  import org.eclipse.jgit.dircache.DirCache;
64  import org.eclipse.jgit.errors.NoMergeBaseException;
65  import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
66  import org.eclipse.jgit.junit.RepositoryTestCase;
67  import org.eclipse.jgit.junit.TestRepository;
68  import org.eclipse.jgit.lib.ObjectId;
69  import org.eclipse.jgit.lib.ObjectInserter;
70  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
71  import org.eclipse.jgit.revwalk.RevCommit;
72  import org.eclipse.jgit.revwalk.RevObject;
73  import org.eclipse.jgit.revwalk.RevTree;
74  import org.eclipse.jgit.revwalk.RevWalk;
75  import org.eclipse.jgit.treewalk.FileTreeIterator;
76  import org.eclipse.jgit.util.FS;
77  import org.eclipse.jgit.util.FileUtils;
78  import org.junit.Assert;
79  import org.junit.experimental.theories.DataPoint;
80  import org.junit.experimental.theories.Theories;
81  import org.junit.experimental.theories.Theory;
82  import org.junit.runner.RunWith;
83  
84  @RunWith(Theories.class)
85  public class ResolveMergerTest extends RepositoryTestCase {
86  
87  	@DataPoint
88  	public static MergeStrategy resolve = MergeStrategy.RESOLVE;
89  
90  	@DataPoint
91  	public static MergeStrategy recursive = MergeStrategy.RECURSIVE;
92  
93  	@Theory
94  	public void failingDeleteOfDirectoryWithUntrackedContent(
95  			MergeStrategy strategy) throws Exception {
96  		File folder1 = new File(db.getWorkTree(), "folder1");
97  		FileUtils.mkdir(folder1);
98  		File file = new File(folder1, "file1.txt");
99  		write(file, "folder1--file1.txt");
100 		file = new File(folder1, "file2.txt");
101 		write(file, "folder1--file2.txt");
102 
103 		try (Git git = new Git(db)) {
104 			git.add().addFilepattern(folder1.getName()).call();
105 			RevCommit base = git.commit().setMessage("adding folder").call();
106 
107 			recursiveDelete(folder1);
108 			git.rm().addFilepattern("folder1/file1.txt")
109 					.addFilepattern("folder1/file2.txt").call();
110 			RevCommit other = git.commit()
111 					.setMessage("removing folders on 'other'").call();
112 
113 			git.checkout().setName(base.name()).call();
114 
115 			file = new File(db.getWorkTree(), "unrelated.txt");
116 			write(file, "unrelated");
117 
118 			git.add().addFilepattern("unrelated.txt").call();
119 			RevCommit head = git.commit().setMessage("Adding another file").call();
120 
121 			// Untracked file to cause failing path for delete() of folder1
122 			// but that's ok.
123 			file = new File(folder1, "file3.txt");
124 			write(file, "folder1--file3.txt");
125 
126 			ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
127 			merger.setCommitNames(new String[] { "BASE", "HEAD", "other" });
128 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
129 			boolean ok = merger.merge(head.getId(), other.getId());
130 			assertTrue(ok);
131 			assertTrue(file.exists());
132 		}
133 	}
134 
135 	/**
136 	 * Merging two conflicting subtrees when the index does not contain any file
137 	 * in that subtree should lead to a conflicting state.
138 	 *
139 	 * @param strategy
140 	 * @throws Exception
141 	 */
142 	@Theory
143 	public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy)
144 			throws Exception {
145 		Git git = Git.wrap(db);
146 
147 		writeTrashFile("d/1", "orig");
148 		git.add().addFilepattern("d/1").call();
149 		RevCommit first = git.commit().setMessage("added d/1").call();
150 
151 		writeTrashFile("d/1", "master");
152 		RevCommit masterCommit = git.commit().setAll(true)
153 				.setMessage("modified d/1 on master").call();
154 
155 		git.checkout().setCreateBranch(true).setStartPoint(first)
156 				.setName("side").call();
157 		writeTrashFile("d/1", "side");
158 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
159 
160 		git.rm().addFilepattern("d/1").call();
161 		git.rm().addFilepattern("d").call();
162 		MergeResult mergeRes = git.merge().setStrategy(strategy)
163 				.include(masterCommit).call();
164 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
165 		assertEquals(
166 				"[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
167 				indexState(CONTENT));
168 	}
169 
170 	/**
171 	 * Merging two different but mergeable subtrees when the index does not
172 	 * contain any file in that subtree should lead to a merged state.
173 	 *
174 	 * @param strategy
175 	 * @throws Exception
176 	 */
177 	@Theory
178 	public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy)
179 			throws Exception {
180 		Git git = Git.wrap(db);
181 
182 		writeTrashFile("d/1", "1\n2\n3");
183 		git.add().addFilepattern("d/1").call();
184 		RevCommit first = git.commit().setMessage("added d/1").call();
185 
186 		writeTrashFile("d/1", "1master\n2\n3");
187 		RevCommit masterCommit = git.commit().setAll(true)
188 				.setMessage("modified d/1 on master").call();
189 
190 		git.checkout().setCreateBranch(true).setStartPoint(first)
191 				.setName("side").call();
192 		writeTrashFile("d/1", "1\n2\n3side");
193 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
194 
195 		git.rm().addFilepattern("d/1").call();
196 		git.rm().addFilepattern("d").call();
197 		MergeResult mergeRes = git.merge().setStrategy(strategy)
198 				.include(masterCommit).call();
199 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
200 		assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
201 				indexState(CONTENT));
202 	}
203 
204 	/**
205 	 * An existing directory without tracked content should not prevent merging
206 	 * a tree where that directory exists.
207 	 *
208 	 * @param strategy
209 	 * @throws Exception
210 	 */
211 	@Theory
212 	public void checkUntrackedFolderIsNotAConflict(
213 			MergeStrategy strategy) throws Exception {
214 		Git git = Git.wrap(db);
215 
216 		writeTrashFile("d/1", "1");
217 		git.add().addFilepattern("d/1").call();
218 		RevCommit first = git.commit().setMessage("added d/1").call();
219 
220 		writeTrashFile("e/1", "4");
221 		git.add().addFilepattern("e/1").call();
222 		RevCommit masterCommit = git.commit().setMessage("added e/1").call();
223 
224 		git.checkout().setCreateBranch(true).setStartPoint(first)
225 				.setName("side").call();
226 		writeTrashFile("f/1", "5");
227 		git.add().addFilepattern("f/1").call();
228 		git.commit().setAll(true).setMessage("added f/1")
229 				.call();
230 
231 		// Untracked directory e shall not conflict with merged e/1
232 		writeTrashFile("e/2", "d two");
233 
234 		MergeResult mergeRes = git.merge().setStrategy(strategy)
235 				.include(masterCommit).call();
236 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
237 		assertEquals(
238 				"[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]",
239 				indexState(CONTENT));
240 	}
241 
242 	/**
243 	 * A tracked file is replaced by a folder in THEIRS.
244 	 *
245 	 * @param strategy
246 	 * @throws Exception
247 	 */
248 	@Theory
249 	public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy)
250 			throws Exception {
251 		Git git = Git.wrap(db);
252 
253 		writeTrashFile("sub", "file");
254 		git.add().addFilepattern("sub").call();
255 		RevCommit first = git.commit().setMessage("initial").call();
256 
257 		git.checkout().setCreateBranch(true).setStartPoint(first)
258 				.setName("side").call();
259 
260 		git.rm().addFilepattern("sub").call();
261 		writeTrashFile("sub/file", "subfile");
262 		git.add().addFilepattern("sub/file").call();
263 		RevCommit masterCommit = git.commit().setMessage("file -> folder")
264 				.call();
265 
266 		git.checkout().setName("master").call();
267 		writeTrashFile("noop", "other");
268 		git.add().addFilepattern("noop").call();
269 		git.commit().setAll(true).setMessage("noop").call();
270 
271 		MergeResult mergeRes = git.merge().setStrategy(strategy)
272 				.include(masterCommit).call();
273 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
274 		assertEquals(
275 				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
276 				indexState(CONTENT));
277 	}
278 
279 	/**
280 	 * A tracked file is replaced by a folder in OURS.
281 	 *
282 	 * @param strategy
283 	 * @throws Exception
284 	 */
285 	@Theory
286 	public void checkFileReplacedByFolderInOurs(MergeStrategy strategy)
287 			throws Exception {
288 		Git git = Git.wrap(db);
289 
290 		writeTrashFile("sub", "file");
291 		git.add().addFilepattern("sub").call();
292 		RevCommit first = git.commit().setMessage("initial").call();
293 
294 		git.checkout().setCreateBranch(true).setStartPoint(first)
295 				.setName("side").call();
296 		writeTrashFile("noop", "other");
297 		git.add().addFilepattern("noop").call();
298 		RevCommit sideCommit = git.commit().setAll(true).setMessage("noop")
299 				.call();
300 
301 		git.checkout().setName("master").call();
302 		git.rm().addFilepattern("sub").call();
303 		writeTrashFile("sub/file", "subfile");
304 		git.add().addFilepattern("sub/file").call();
305 		git.commit().setMessage("file -> folder")
306 				.call();
307 
308 		MergeResult mergeRes = git.merge().setStrategy(strategy)
309 				.include(sideCommit).call();
310 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
311 		assertEquals(
312 				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
313 				indexState(CONTENT));
314 	}
315 
316 	/**
317 	 * An existing directory without tracked content should not prevent merging
318 	 * a file with that name.
319 	 *
320 	 * @param strategy
321 	 * @throws Exception
322 	 */
323 	@Theory
324 	public void checkUntrackedEmpytFolderIsNotAConflictWithFile(
325 			MergeStrategy strategy)
326 			throws Exception {
327 		Git git = Git.wrap(db);
328 
329 		writeTrashFile("d/1", "1");
330 		git.add().addFilepattern("d/1").call();
331 		RevCommit first = git.commit().setMessage("added d/1").call();
332 
333 		writeTrashFile("e", "4");
334 		git.add().addFilepattern("e").call();
335 		RevCommit masterCommit = git.commit().setMessage("added e").call();
336 
337 		git.checkout().setCreateBranch(true).setStartPoint(first)
338 				.setName("side").call();
339 		writeTrashFile("f/1", "5");
340 		git.add().addFilepattern("f/1").call();
341 		git.commit().setAll(true).setMessage("added f/1").call();
342 
343 		// Untracked empty directory hierarcy e/1 shall not conflict with merged
344 		// e/1
345 		FileUtils.mkdirs(new File(trash, "e/1"), true);
346 
347 		MergeResult mergeRes = git.merge().setStrategy(strategy)
348 				.include(masterCommit).call();
349 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
350 		assertEquals(
351 				"[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]",
352 				indexState(CONTENT));
353 	}
354 
355 	@Theory
356 	public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException,
357 			GitAPIException {
358 		Git git = Git.wrap(db);
359 		db.getConfig().setString("core", null, "autocrlf", "false");
360 		db.getConfig().save();
361 		writeTrashFile("crlf.txt", "some\r\ndata\r\n");
362 		git.add().addFilepattern("crlf.txt").call();
363 		git.commit().setMessage("base").call();
364 
365 		git.branchCreate().setName("brancha").call();
366 
367 		writeTrashFile("crlf.txt", "some\r\nmore\r\ndata\r\n");
368 		git.add().addFilepattern("crlf.txt").call();
369 		git.commit().setMessage("on master").call();
370 
371 		git.checkout().setName("brancha").call();
372 		writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n");
373 		git.add().addFilepattern("crlf.txt").call();
374 		git.commit().setMessage("on brancha").call();
375 
376 		db.getConfig().setString("core", null, "autocrlf", "input");
377 		db.getConfig().save();
378 
379 		MergeResult mergeResult = git.merge().setStrategy(strategy)
380 				.include(db.resolve("master"))
381 				.call();
382 		assertEquals(MergeResult.MergeStatus.MERGED,
383 				mergeResult.getMergeStatus());
384 	}
385 
386 	/**
387 	 * Merging two equal subtrees when the index does not contain any file in
388 	 * that subtree should lead to a merged state.
389 	 *
390 	 * @param strategy
391 	 * @throws Exception
392 	 */
393 	@Theory
394 	public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
395 			throws Exception {
396 		Git git = Git.wrap(db);
397 
398 		writeTrashFile("d/1", "orig");
399 		git.add().addFilepattern("d/1").call();
400 		RevCommit first = git.commit().setMessage("added d/1").call();
401 
402 		writeTrashFile("d/1", "modified");
403 		RevCommit masterCommit = git.commit().setAll(true)
404 				.setMessage("modified d/1 on master").call();
405 
406 		git.checkout().setCreateBranch(true).setStartPoint(first)
407 				.setName("side").call();
408 		writeTrashFile("d/1", "modified");
409 		git.commit().setAll(true).setMessage("modified d/1 on side").call();
410 
411 		git.rm().addFilepattern("d/1").call();
412 		git.rm().addFilepattern("d").call();
413 		MergeResult mergeRes = git.merge().setStrategy(strategy)
414 				.include(masterCommit).call();
415 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
416 		assertEquals("[d/1, mode:100644, content:modified]",
417 				indexState(CONTENT));
418 	}
419 
420 	/**
421 	 * Merging two equal subtrees with an incore merger should lead to a merged
422 	 * state.
423 	 *
424 	 * @param strategy
425 	 * @throws Exception
426 	 */
427 	@Theory
428 	public void checkMergeEqualTreesInCore(MergeStrategy strategy)
429 			throws Exception {
430 		Git git = Git.wrap(db);
431 
432 		writeTrashFile("d/1", "orig");
433 		git.add().addFilepattern("d/1").call();
434 		RevCommit first = git.commit().setMessage("added d/1").call();
435 
436 		writeTrashFile("d/1", "modified");
437 		RevCommit masterCommit = git.commit().setAll(true)
438 				.setMessage("modified d/1 on master").call();
439 
440 		git.checkout().setCreateBranch(true).setStartPoint(first)
441 				.setName("side").call();
442 		writeTrashFile("d/1", "modified");
443 		RevCommit sideCommit = git.commit().setAll(true)
444 				.setMessage("modified d/1 on side").call();
445 
446 		git.rm().addFilepattern("d/1").call();
447 		git.rm().addFilepattern("d").call();
448 
449 		ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
450 				true);
451 		boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
452 		assertTrue(noProblems);
453 	}
454 
455 	/**
456 	 * Merging two equal subtrees with an incore merger should lead to a merged
457 	 * state, without using a Repository (the 'Gerrit' use case).
458 	 *
459 	 * @param strategy
460 	 * @throws Exception
461 	 */
462 	@Theory
463 	public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy)
464 			throws Exception {
465 		Git git = Git.wrap(db);
466 
467 		writeTrashFile("d/1", "orig");
468 		git.add().addFilepattern("d/1").call();
469 		RevCommit first = git.commit().setMessage("added d/1").call();
470 
471 		writeTrashFile("d/1", "modified");
472 		RevCommit masterCommit = git.commit().setAll(true)
473 				.setMessage("modified d/1 on master").call();
474 
475 		git.checkout().setCreateBranch(true).setStartPoint(first)
476 				.setName("side").call();
477 		writeTrashFile("d/1", "modified");
478 		RevCommit sideCommit = git.commit().setAll(true)
479 				.setMessage("modified d/1 on side").call();
480 
481 		git.rm().addFilepattern("d/1").call();
482 		git.rm().addFilepattern("d").call();
483 
484 		try (ObjectInserter ins = db.newObjectInserter()) {
485 			ThreeWayMerger resolveMerger =
486 					(ThreeWayMerger) strategy.newMerger(ins, db.getConfig());
487 			boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
488 			assertTrue(noProblems);
489 		}
490 	}
491 
492 	/**
493 	 * Merging two equal subtrees when the index and HEAD does not contain any
494 	 * file in that subtree should lead to a merged state.
495 	 *
496 	 * @param strategy
497 	 * @throws Exception
498 	 */
499 	@Theory
500 	public void checkMergeEqualNewTrees(MergeStrategy strategy)
501 			throws Exception {
502 		Git git = Git.wrap(db);
503 
504 		writeTrashFile("2", "orig");
505 		git.add().addFilepattern("2").call();
506 		RevCommit first = git.commit().setMessage("added 2").call();
507 
508 		writeTrashFile("d/1", "orig");
509 		git.add().addFilepattern("d/1").call();
510 		RevCommit masterCommit = git.commit().setAll(true)
511 				.setMessage("added d/1 on master").call();
512 
513 		git.checkout().setCreateBranch(true).setStartPoint(first)
514 				.setName("side").call();
515 		writeTrashFile("d/1", "orig");
516 		git.add().addFilepattern("d/1").call();
517 		git.commit().setAll(true).setMessage("added d/1 on side").call();
518 
519 		git.rm().addFilepattern("d/1").call();
520 		git.rm().addFilepattern("d").call();
521 		MergeResult mergeRes = git.merge().setStrategy(strategy)
522 				.include(masterCommit).call();
523 		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
524 		assertEquals(
525 				"[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
526 				indexState(CONTENT));
527 	}
528 
529 	/**
530 	 * Merging two conflicting subtrees when the index and HEAD does not contain
531 	 * any file in that subtree should lead to a conflicting state.
532 	 *
533 	 * @param strategy
534 	 * @throws Exception
535 	 */
536 	@Theory
537 	public void checkMergeConflictingNewTrees(MergeStrategy strategy)
538 			throws Exception {
539 		Git git = Git.wrap(db);
540 
541 		writeTrashFile("2", "orig");
542 		git.add().addFilepattern("2").call();
543 		RevCommit first = git.commit().setMessage("added 2").call();
544 
545 		writeTrashFile("d/1", "master");
546 		git.add().addFilepattern("d/1").call();
547 		RevCommit masterCommit = git.commit().setAll(true)
548 				.setMessage("added d/1 on master").call();
549 
550 		git.checkout().setCreateBranch(true).setStartPoint(first)
551 				.setName("side").call();
552 		writeTrashFile("d/1", "side");
553 		git.add().addFilepattern("d/1").call();
554 		git.commit().setAll(true).setMessage("added d/1 on side").call();
555 
556 		git.rm().addFilepattern("d/1").call();
557 		git.rm().addFilepattern("d").call();
558 		MergeResult mergeRes = git.merge().setStrategy(strategy)
559 				.include(masterCommit).call();
560 		assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
561 		assertEquals(
562 				"[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
563 				indexState(CONTENT));
564 	}
565 
566 	/**
567 	 * Merging two conflicting files when the index contains a tree for that
568 	 * path should lead to a failed state.
569 	 *
570 	 * @param strategy
571 	 * @throws Exception
572 	 */
573 	@Theory
574 	public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
575 			throws Exception {
576 		Git git = Git.wrap(db);
577 
578 		writeTrashFile("0", "orig");
579 		git.add().addFilepattern("0").call();
580 		RevCommit first = git.commit().setMessage("added 0").call();
581 
582 		writeTrashFile("0", "master");
583 		RevCommit masterCommit = git.commit().setAll(true)
584 				.setMessage("modified 0 on master").call();
585 
586 		git.checkout().setCreateBranch(true).setStartPoint(first)
587 				.setName("side").call();
588 		writeTrashFile("0", "side");
589 		git.commit().setAll(true).setMessage("modified 0 on side").call();
590 
591 		git.rm().addFilepattern("0").call();
592 		writeTrashFile("0/0", "side");
593 		git.add().addFilepattern("0/0").call();
594 		MergeResult mergeRes = git.merge().setStrategy(strategy)
595 				.include(masterCommit).call();
596 		assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
597 	}
598 
599 	/**
600 	 * Merging two equal files when the index contains a tree for that path
601 	 * should lead to a failed state.
602 	 *
603 	 * @param strategy
604 	 * @throws Exception
605 	 */
606 	@Theory
607 	public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
608 			throws Exception {
609 		Git git = Git.wrap(db);
610 
611 		writeTrashFile("0", "orig");
612 		writeTrashFile("1", "1\n2\n3");
613 		git.add().addFilepattern("0").addFilepattern("1").call();
614 		RevCommit first = git.commit().setMessage("added 0, 1").call();
615 
616 		writeTrashFile("1", "1master\n2\n3");
617 		RevCommit masterCommit = git.commit().setAll(true)
618 				.setMessage("modified 1 on master").call();
619 
620 		git.checkout().setCreateBranch(true).setStartPoint(first)
621 				.setName("side").call();
622 		writeTrashFile("1", "1\n2\n3side");
623 		git.commit().setAll(true).setMessage("modified 1 on side").call();
624 
625 		git.rm().addFilepattern("0").call();
626 		writeTrashFile("0/0", "modified");
627 		git.add().addFilepattern("0/0").call();
628 		try {
629 			git.merge().setStrategy(strategy).include(masterCommit).call();
630 			Assert.fail("Didn't get the expected exception");
631 		} catch (CheckoutConflictException e) {
632 			assertEquals(1, e.getConflictingPaths().size());
633 			assertEquals("0/0", e.getConflictingPaths().get(0));
634 		}
635 	}
636 
637 	@Theory
638 	public void checkContentMergeNoConflict(MergeStrategy strategy)
639 			throws Exception {
640 		Git git = Git.wrap(db);
641 
642 		writeTrashFile("file", "1\n2\n3");
643 		git.add().addFilepattern("file").call();
644 		RevCommit first = git.commit().setMessage("added file").call();
645 
646 		writeTrashFile("file", "1master\n2\n3");
647 		git.commit().setAll(true).setMessage("modified file on master").call();
648 
649 		git.checkout().setCreateBranch(true).setStartPoint(first)
650 				.setName("side").call();
651 		writeTrashFile("file", "1\n2\n3side");
652 		RevCommit sideCommit = git.commit().setAll(true)
653 				.setMessage("modified file on side").call();
654 
655 		git.checkout().setName("master").call();
656 		MergeResult result =
657 				git.merge().setStrategy(strategy).include(sideCommit).call();
658 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
659 		String expected = "1master\n2\n3side";
660 		assertEquals(expected, read("file"));
661 	}
662 
663 	@Theory
664 	public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy)
665 			throws Exception {
666 		Git git = Git.wrap(db);
667 
668 		writeTrashFile("file", "1\n2\n3");
669 		git.add().addFilepattern("file").call();
670 		RevCommit first = git.commit().setMessage("added file").call();
671 
672 		writeTrashFile("file", "1master\n2\n3");
673 		RevCommit masterCommit = git.commit().setAll(true)
674 				.setMessage("modified file on master").call();
675 
676 		git.checkout().setCreateBranch(true).setStartPoint(first)
677 				.setName("side").call();
678 		writeTrashFile("file", "1\n2\n3side");
679 		RevCommit sideCommit = git.commit().setAll(true)
680 				.setMessage("modified file on side").call();
681 
682 		try (ObjectInserter ins = db.newObjectInserter()) {
683 			ResolveMerger merger =
684 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
685 			boolean noProblems = merger.merge(masterCommit, sideCommit);
686 			assertTrue(noProblems);
687 			assertEquals("1master\n2\n3side",
688 					readBlob(merger.getResultTreeId(), "file"));
689 		}
690 	}
691 
692 	@Theory
693 	public void checkContentMergeConflict(MergeStrategy strategy)
694 			throws Exception {
695 		Git git = Git.wrap(db);
696 
697 		writeTrashFile("file", "1\n2\n3");
698 		git.add().addFilepattern("file").call();
699 		RevCommit first = git.commit().setMessage("added file").call();
700 
701 		writeTrashFile("file", "1master\n2\n3");
702 		git.commit().setAll(true).setMessage("modified file on master").call();
703 
704 		git.checkout().setCreateBranch(true).setStartPoint(first)
705 				.setName("side").call();
706 		writeTrashFile("file", "1side\n2\n3");
707 		RevCommit sideCommit = git.commit().setAll(true)
708 				.setMessage("modified file on side").call();
709 
710 		git.checkout().setName("master").call();
711 		MergeResult result =
712 				git.merge().setStrategy(strategy).include(sideCommit).call();
713 		assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
714 		String expected = "<<<<<<< HEAD\n"
715 				+ "1master\n"
716 				+ "=======\n"
717 				+ "1side\n"
718 				+ ">>>>>>> " + sideCommit.name() + "\n"
719 				+ "2\n"
720 				+ "3";
721 		assertEquals(expected, read("file"));
722 	}
723 
724 	@Theory
725 	public void checkContentMergeConflict_noTree(MergeStrategy strategy)
726 			throws Exception {
727 		Git git = Git.wrap(db);
728 
729 		writeTrashFile("file", "1\n2\n3");
730 		git.add().addFilepattern("file").call();
731 		RevCommit first = git.commit().setMessage("added file").call();
732 
733 		writeTrashFile("file", "1master\n2\n3");
734 		RevCommit masterCommit = git.commit().setAll(true)
735 				.setMessage("modified file on master").call();
736 
737 		git.checkout().setCreateBranch(true).setStartPoint(first)
738 				.setName("side").call();
739 		writeTrashFile("file", "1side\n2\n3");
740 		RevCommit sideCommit = git.commit().setAll(true)
741 				.setMessage("modified file on side").call();
742 
743 		try (ObjectInserter ins = db.newObjectInserter()) {
744 			ResolveMerger merger =
745 					(ResolveMerger) strategy.newMerger(ins, db.getConfig());
746 			boolean noProblems = merger.merge(masterCommit, sideCommit);
747 			assertFalse(noProblems);
748 			assertEquals(Arrays.asList("file"), merger.getUnmergedPaths());
749 
750 			MergeFormatter fmt = new MergeFormatter();
751 			merger.getMergeResults().get("file");
752 			try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
753 				fmt.formatMerge(out, merger.getMergeResults().get("file"),
754 						"BASE", "OURS", "THEIRS", UTF_8.name());
755 				String expected = "<<<<<<< OURS\n"
756 						+ "1master\n"
757 						+ "=======\n"
758 						+ "1side\n"
759 						+ ">>>>>>> THEIRS\n"
760 						+ "2\n"
761 						+ "3";
762 				assertEquals(expected, new String(out.toByteArray(), UTF_8));
763 			}
764 		}
765 	}
766 
767 	/**
768 	 * Merging after criss-cross merges. In this case we merge together two
769 	 * commits which have two equally good common ancestors
770 	 *
771 	 * @param strategy
772 	 * @throws Exception
773 	 */
774 	@Theory
775 	public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
776 		Git git = Git.wrap(db);
777 
778 		writeTrashFile("1", "1\n2\n3");
779 		git.add().addFilepattern("1").call();
780 		RevCommit first = git.commit().setMessage("added 1").call();
781 
782 		writeTrashFile("1", "1master\n2\n3");
783 		RevCommit masterCommit = git.commit().setAll(true)
784 				.setMessage("modified 1 on master").call();
785 
786 		writeTrashFile("1", "1master2\n2\n3");
787 		git.commit().setAll(true)
788 				.setMessage("modified 1 on master again").call();
789 
790 		git.checkout().setCreateBranch(true).setStartPoint(first)
791 				.setName("side").call();
792 		writeTrashFile("1", "1\n2\na\nb\nc\n3side");
793 		RevCommit sideCommit = git.commit().setAll(true)
794 				.setMessage("modified 1 on side").call();
795 
796 		writeTrashFile("1", "1\n2\n3side2");
797 		git.commit().setAll(true)
798 				.setMessage("modified 1 on side again").call();
799 
800 		MergeResult result = git.merge().setStrategy(strategy)
801 				.include(masterCommit).call();
802 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
803 		result.getNewHead();
804 		git.checkout().setName("master").call();
805 		result = git.merge().setStrategy(strategy).include(sideCommit).call();
806 		assertEquals(MergeStatus.MERGED, result.getMergeStatus());
807 
808 		// we have two branches which are criss-cross merged. Try to merge the
809 		// tips. This should succeed with RecursiveMerge and fail with
810 		// ResolveMerge
811 		try {
812 			MergeResult mergeResult = git.merge().setStrategy(strategy)
813 					.include(git.getRepository().exactRef("refs/heads/side"))
814 					.call();
815 			assertEquals(MergeStrategy.RECURSIVE, strategy);
816 			assertEquals(MergeResult.MergeStatus.MERGED,
817 					mergeResult.getMergeStatus());
818 			assertEquals("1master2\n2\n3side2", read("1"));
819 		} catch (JGitInternalException e) {
820 			assertEquals(MergeStrategy.RESOLVE, strategy);
821 			assertTrue(e.getCause() instanceof NoMergeBaseException);
822 			assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
823 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
824 		}
825 	}
826 
827 	@Theory
828 	public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
829 			throws Exception {
830 		Git git = Git.wrap(db);
831 
832 		writeTrashFile("a.txt", "orig");
833 		writeTrashFile("b.txt", "orig");
834 		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
835 		RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
836 
837 		// modify and delete files on the master branch
838 		writeTrashFile("a.txt", "master");
839 		git.rm().addFilepattern("b.txt").call();
840 		RevCommit masterCommit = git.commit()
841 				.setMessage("modified a.txt, deleted b.txt").setAll(true)
842 				.call();
843 
844 		// switch back to a side branch
845 		git.checkout().setCreateBranch(true).setStartPoint(first)
846 				.setName("side").call();
847 		writeTrashFile("c.txt", "side");
848 		git.add().addFilepattern("c.txt").call();
849 		git.commit().setMessage("added c.txt").call();
850 
851 		// Get a handle to the the file so on windows it can't be deleted.
852 		FileInputStream fis = new FileInputStream(new File(db.getWorkTree(),
853 				"b.txt"));
854 		MergeResult mergeRes = git.merge().setStrategy(strategy)
855 				.include(masterCommit).call();
856 		if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
857 			// probably windows
858 			assertEquals(1, mergeRes.getFailingPaths().size());
859 			assertEquals(MergeFailureReason.COULD_NOT_DELETE, mergeRes
860 					.getFailingPaths().get("b.txt"));
861 		}
862 		assertEquals("[a.txt, mode:100644, content:master]"
863 				+ "[c.txt, mode:100644, content:side]", indexState(CONTENT));
864 		fis.close();
865 	}
866 
867 	@Theory
868 	public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
869 		File f;
870 		long lastTs4, lastTsIndex;
871 		Git git = Git.wrap(db);
872 		File indexFile = db.getIndexFile();
873 
874 		// Create initial content and remember when the last file was written.
875 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
876 		lastTs4 = FS.DETECTED.lastModified(f);
877 
878 		// add all files, commit and check this doesn't update any working tree
879 		// files and that the index is in a new file system timer tick. Make
880 		// sure to wait long enough before adding so the index doesn't contain
881 		// racily clean entries
882 		fsTick(f);
883 		git.add().addFilepattern(".").call();
884 		RevCommit firstCommit = git.commit().setMessage("initial commit")
885 				.call();
886 		checkConsistentLastModified("0", "1", "2", "3", "4");
887 		checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
888 		assertEquals("Commit should not touch working tree file 4", lastTs4,
889 				FS.DETECTED.lastModified(new File(db.getWorkTree(), "4")));
890 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
891 
892 		// Do modifications on the master branch. Then add and commit. This
893 		// should touch only "0", "2 and "3"
894 		fsTick(indexFile);
895 		f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
896 				null);
897 		fsTick(f);
898 		git.add().addFilepattern(".").call();
899 		RevCommit masterCommit = git.commit().setMessage("master commit")
900 				.call();
901 		checkConsistentLastModified("0", "1", "2", "3", "4");
902 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
903 				+ lastTsIndex, "<0", "2", "3", "<.git/index");
904 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
905 
906 		// Checkout a side branch. This should touch only "0", "2 and "3"
907 		fsTick(indexFile);
908 		git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
909 				.setName("side").call();
910 		checkConsistentLastModified("0", "1", "2", "3", "4");
911 		checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
912 				+ lastTsIndex, "<0", "2", "3", ".git/index");
913 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
914 
915 		// This checkout may have populated worktree and index so fast that we
916 		// may have smudged entries now. Check that we have the right content
917 		// and then rewrite the index to get rid of smudged state
918 		assertEquals("[0, mode:100644, content:orig]" //
919 				+ "[1, mode:100644, content:orig]" //
920 				+ "[2, mode:100644, content:1\n2\n3]" //
921 				+ "[3, mode:100644, content:orig]" //
922 				+ "[4, mode:100644, content:orig]", //
923 				indexState(CONTENT));
924 		fsTick(indexFile);
925 		f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
926 		lastTs4 = FS.DETECTED.lastModified(f);
927 		fsTick(f);
928 		git.add().addFilepattern(".").call();
929 		checkConsistentLastModified("0", "1", "2", "3", "4");
930 		checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
931 				"4", "<.git/index");
932 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
933 
934 		// Do modifications on the side branch. Touch only "1", "2 and "3"
935 		fsTick(indexFile);
936 		f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
937 		fsTick(f);
938 		git.add().addFilepattern(".").call();
939 		git.commit().setMessage("side commit").call();
940 		checkConsistentLastModified("0", "1", "2", "3", "4");
941 		checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
942 				+ lastTsIndex, "<1", "2", "3", "<.git/index");
943 		lastTsIndex = FS.DETECTED.lastModified(indexFile);
944 
945 		// merge master and side. Should only touch "0," "2" and "3"
946 		fsTick(indexFile);
947 		git.merge().setStrategy(strategy).include(masterCommit).call();
948 		checkConsistentLastModified("0", "1", "2", "4");
949 		checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
950 				+ lastTsIndex, "<0", "2", "3", ".git/index");
951 		assertEquals(
952 				"[0, mode:100644, content:master]" //
953 						+ "[1, mode:100644, content:side]" //
954 						+ "[2, mode:100644, content:1master\n2\n3side]" //
955 						+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
956 						+ "[4, mode:100644, content:orig]", //
957 				indexState(CONTENT));
958 	}
959 
960 	// Assert that every specified index entry has the same last modification
961 	// timestamp as the associated file
962 	private void checkConsistentLastModified(String... pathes)
963 			throws IOException {
964 		DirCache dc = db.readDirCache();
965 		File workTree = db.getWorkTree();
966 		for (String path : pathes)
967 			assertEquals(
968 					"IndexEntry with path "
969 							+ path
970 							+ " has lastmodified with is different from the worktree file",
971 					FS.DETECTED.lastModified(new File(workTree, path)), dc.getEntry(path)
972 							.getLastModified());
973 	}
974 
975 	// Assert that modification timestamps of working tree files are as
976 	// expected. You may specify n files. It is asserted that every file
977 	// i+1 is not older than file i. If a path of file i+1 is prefixed with "<"
978 	// then this file must be younger then file i. A path "*<modtime>"
979 	// represents a file with a modification time of <modtime>
980 	// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
981 	private void checkModificationTimeStampOrder(String... pathes)
982 			throws IOException {
983 		long lastMod = Long.MIN_VALUE;
984 		for (String p : pathes) {
985 			boolean strong = p.startsWith("<");
986 			boolean fixed = p.charAt(strong ? 1 : 0) == '*';
987 			p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
988 			long curMod = fixed ? Long.valueOf(p).longValue()
989 					: FS.DETECTED.lastModified(new File(db.getWorkTree(), p));
990 			if (strong)
991 				assertTrue("path " + p + " is not younger than predecesssor",
992 						curMod > lastMod);
993 			else
994 				assertTrue("path " + p + " is older than predecesssor",
995 						curMod >= lastMod);
996 		}
997 	}
998 
999 	private String readBlob(ObjectId treeish, String path) throws Exception {
1000 		TestRepository<?> tr = new TestRepository<>(db);
1001 		RevWalk rw = tr.getRevWalk();
1002 		RevTree tree = rw.parseTree(treeish);
1003 		RevObject obj = tr.get(tree, path);
1004 		if (obj == null) {
1005 			return null;
1006 		}
1007 		return new String(rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(), UTF_8);
1008 	}
1009 }