View Javadoc
1   /*
2    * Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com>
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.assertFalse;
47  import static org.junit.Assert.assertTrue;
48  
49  import java.io.BufferedReader;
50  import java.io.File;
51  import java.io.FileOutputStream;
52  import java.io.IOException;
53  import java.io.InputStreamReader;
54  
55  import org.eclipse.jgit.api.Git;
56  import org.eclipse.jgit.dircache.DirCache;
57  import org.eclipse.jgit.dircache.DirCacheEditor;
58  import org.eclipse.jgit.dircache.DirCacheEntry;
59  import org.eclipse.jgit.errors.MissingObjectException;
60  import org.eclipse.jgit.errors.NoMergeBaseException;
61  import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
62  import org.eclipse.jgit.internal.storage.file.FileRepository;
63  import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
64  import org.eclipse.jgit.junit.RepositoryTestCase;
65  import org.eclipse.jgit.junit.TestRepository;
66  import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
67  import org.eclipse.jgit.lib.AnyObjectId;
68  import org.eclipse.jgit.lib.Constants;
69  import org.eclipse.jgit.lib.FileMode;
70  import org.eclipse.jgit.lib.ObjectId;
71  import org.eclipse.jgit.lib.ObjectLoader;
72  import org.eclipse.jgit.lib.ObjectReader;
73  import org.eclipse.jgit.lib.Repository;
74  import org.eclipse.jgit.revwalk.RevBlob;
75  import org.eclipse.jgit.revwalk.RevCommit;
76  import org.eclipse.jgit.treewalk.FileTreeIterator;
77  import org.eclipse.jgit.treewalk.TreeWalk;
78  import org.eclipse.jgit.treewalk.filter.PathFilter;
79  import org.junit.Before;
80  import org.junit.experimental.theories.DataPoints;
81  import org.junit.experimental.theories.Theories;
82  import org.junit.experimental.theories.Theory;
83  import org.junit.runner.RunWith;
84  
85  @RunWith(Theories.class)
86  public class CrissCrossMergeTest extends RepositoryTestCase {
87  	static int counter = 0;
88  
89  	@DataPoints
90  	public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] {
91  			MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE };
92  
93  	public enum IndexState {
94  		Bare, Missing, SameAsHead, SameAsOther, SameAsWorkTree, DifferentFromHeadAndOtherAndWorktree
95  	}
96  
97  	@DataPoints
98  	public static IndexState[] indexStates = IndexState.values();
99  
100 	public enum WorktreeState {
101 		Bare, Missing, SameAsHead, DifferentFromHeadAndOther, SameAsOther;
102 	}
103 
104 	@DataPoints
105 	public static WorktreeState[] worktreeStates = WorktreeState.values();
106 
107 	private TestRepository<FileRepository> db_t;
108 
109 	@Override
110 	@Before
111 	public void setUp() throws Exception {
112 		super.setUp();
113 		db_t = new TestRepository<>(db);
114 	}
115 
116 	@Theory
117 	/**
118 	 * Merging m2,s2 from the following topology. In master and side different
119 	 * files are touched. No need to do a real content merge.
120 	 *
121 	 * <pre>
122 	 * m0--m1--m2
123 	 *   \   \/
124 	 *    \  /\
125 	 *     s1--s2
126 	 * </pre>
127 	 */
128 	public void crissCrossMerge(MergeStrategy strategy, IndexState indexState,
129 			WorktreeState worktreeState) throws Exception {
130 		if (!validateStates(indexState, worktreeState))
131 			return;
132 		// fill the repo
133 		BranchBuilder master = db_t.branch("master");
134 		RevCommit m0 = master.commit().add("m", ",m0").message("m0").create();
135 		RevCommit m1 = master.commit().add("m", "m1").message("m1").create();
136 		db_t.getRevWalk().parseCommit(m1);
137 
138 		BranchBuilder side = db_t.branch("side");
139 		RevCommit s1 = side.commit().parent(m0).add("s", "s1").message("s1")
140 				.create();
141 		RevCommit s2 = side.commit().parent(m1).add("m", "m1")
142 				.message("s2(merge)").create();
143 		RevCommit m2 = master.commit().parent(s1).add("s", "s1")
144 				.message("m2(merge)").create();
145 
146 		Git git = Git.wrap(db);
147 		git.checkout().setName("master").call();
148 		modifyWorktree(worktreeState, "m", "side");
149 		modifyWorktree(worktreeState, "s", "side");
150 		modifyIndex(indexState, "m", "side");
151 		modifyIndex(indexState, "s", "side");
152 
153 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
154 				worktreeState == WorktreeState.Bare);
155 		if (worktreeState != WorktreeState.Bare)
156 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
157 		try {
158 			boolean expectSuccess = true;
159 			if (!(indexState == IndexState.Bare
160 					|| indexState == IndexState.Missing
161 					|| indexState == IndexState.SameAsHead || indexState == IndexState.SameAsOther))
162 				// index is dirty
163 				expectSuccess = false;
164 
165 			assertEquals(Boolean.valueOf(expectSuccess),
166 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
167 			assertEquals(MergeStrategy.RECURSIVE, strategy);
168 			assertEquals("m1",
169 					contentAsString(db, merger.getResultTreeId(), "m"));
170 			assertEquals("s1",
171 					contentAsString(db, merger.getResultTreeId(), "s"));
172 		} catch (NoMergeBaseException e) {
173 			assertEquals(MergeStrategy.RESOLVE, strategy);
174 			assertEquals(e.getReason(),
175 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
176 		}
177 	}
178 
179 	@Theory
180 	/**
181 	 * Merging m2,s2 from the following topology. m1 and s1 are the two root
182 	 * commits of the repo. In master and side different files are touched.
183 	 * No need to do a real content merge.
184 	 *
185 	 * <pre>
186 	 * m1--m2
187 	 *   \/
188 	 *   /\
189 	 * s1--s2
190 	 * </pre>
191 	 */
192 	public void crissCrossMerge_twoRoots(MergeStrategy strategy,
193 			IndexState indexState, WorktreeState worktreeState)
194 			throws Exception {
195 		if (!validateStates(indexState, worktreeState))
196 			return;
197 		// fill the repo
198 		BranchBuilder master = db_t.branch("master");
199 		BranchBuilder side = db_t.branch("side");
200 		RevCommit m1 = master.commit().add("m", "m1").message("m1").create();
201 		db_t.getRevWalk().parseCommit(m1);
202 
203 		RevCommit s1 = side.commit().add("s", "s1").message("s1").create();
204 		RevCommit s2 = side.commit().parent(m1).add("m", "m1")
205 				.message("s2(merge)").create();
206 		RevCommit m2 = master.commit().parent(s1).add("s", "s1")
207 				.message("m2(merge)").create();
208 
209 		Git git = Git.wrap(db);
210 		git.checkout().setName("master").call();
211 		modifyWorktree(worktreeState, "m", "side");
212 		modifyWorktree(worktreeState, "s", "side");
213 		modifyIndex(indexState, "m", "side");
214 		modifyIndex(indexState, "s", "side");
215 
216 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
217 				worktreeState == WorktreeState.Bare);
218 		if (worktreeState != WorktreeState.Bare)
219 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
220 		try {
221 			boolean expectSuccess = true;
222 			if (!(indexState == IndexState.Bare
223 					|| indexState == IndexState.Missing
224 					|| indexState == IndexState.SameAsHead || indexState == IndexState.SameAsOther))
225 				// index is dirty
226 				expectSuccess = false;
227 
228 			assertEquals(Boolean.valueOf(expectSuccess),
229 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
230 			assertEquals(MergeStrategy.RECURSIVE, strategy);
231 			assertEquals("m1",
232 					contentAsString(db, merger.getResultTreeId(), "m"));
233 			assertEquals("s1",
234 					contentAsString(db, merger.getResultTreeId(), "s"));
235 		} catch (NoMergeBaseException e) {
236 			assertEquals(MergeStrategy.RESOLVE, strategy);
237 			assertEquals(e.getReason(),
238 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
239 		}
240 	}
241 
242 	@Theory
243 	/**
244 	 * Merging m2,s2 from the following topology. The same file is modified
245 	 * in both branches. The modifications should be mergeable. m2 and s2
246 	 * contain branch specific conflict resolutions. Therefore m2 and s2 don't contain the same content.
247 	 *
248 	 * <pre>
249 	 * m0--m1--m2
250 	 *   \   \/
251 	 *    \  /\
252 	 *     s1--s2
253 	 * </pre>
254 	 */
255 	public void crissCrossMerge_mergeable(MergeStrategy strategy,
256 			IndexState indexState, WorktreeState worktreeState)
257 			throws Exception {
258 		if (!validateStates(indexState, worktreeState))
259 			return;
260 
261 		BranchBuilder master = db_t.branch("master");
262 		RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
263 				.message("m0").create();
264 		RevCommit m1 = master.commit()
265 				.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1")
266 				.create();
267 		db_t.getRevWalk().parseCommit(m1);
268 
269 		BranchBuilder side = db_t.branch("side");
270 		RevCommit s1 = side.commit().parent(m0)
271 				.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1")
272 				.create();
273 		RevCommit s2 = side.commit().parent(m1)
274 				.add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n")
275 				.message("s2(merge)").create();
276 		RevCommit m2 = master
277 				.commit()
278 				.parent(s1)
279 				.add("f", "1-master\n2\n3-res(master)\n4\n5\n6\n7\n8\n9-side\n")
280 				.message("m2(merge)").create();
281 
282 		Git git = Git.wrap(db);
283 		git.checkout().setName("master").call();
284 		modifyWorktree(worktreeState, "f", "side");
285 		modifyIndex(indexState, "f", "side");
286 
287 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
288 				worktreeState == WorktreeState.Bare);
289 		if (worktreeState != WorktreeState.Bare)
290 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
291 		try {
292 			boolean expectSuccess = true;
293 			if (!(indexState == IndexState.Bare
294 					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
295 				// index is dirty
296 				expectSuccess = false;
297 			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
298 					|| worktreeState == WorktreeState.SameAsOther)
299 				expectSuccess = false;
300 			assertEquals(Boolean.valueOf(expectSuccess),
301 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
302 			assertEquals(MergeStrategy.RECURSIVE, strategy);
303 			if (!expectSuccess)
304 				// if the merge was not successful skip testing the state of index and workingtree
305 				return;
306 			assertEquals(
307 					"1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side",
308 					contentAsString(db, merger.getResultTreeId(), "f"));
309 			if (indexState != IndexState.Bare)
310 				assertEquals(
311 						"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n]",
312 						indexState(LocalDiskRepositoryTestCase.CONTENT));
313 			if (worktreeState != WorktreeState.Bare
314 					&& worktreeState != WorktreeState.Missing)
315 				assertEquals(
316 						"1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n",
317 						read("f"));
318 		} catch (NoMergeBaseException e) {
319 			assertEquals(MergeStrategy.RESOLVE, strategy);
320 			assertEquals(e.getReason(),
321 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
322 		}
323 	}
324 
325 	@Theory
326 	/**
327 	 * Merging m2,s2 from the following topology. The same file is modified
328 	 * in both branches. The modifications should be mergeable but only if the automerge of m1 and s1
329 	 * is choosen as parent. Choosing m0 as parent would not be sufficient (in contrast to the merge in
330 	 * crissCrossMerge_mergeable). m2 and s2 contain branch specific conflict resolutions. Therefore m2
331 	 * and s2 don't contain the same content.
332 	 *
333 	 * <pre>
334 	 * m0--m1--m2
335 	 *   \   \/
336 	 *    \  /\
337 	 *     s1--s2
338 	 * </pre>
339 	 */
340 	public void crissCrossMerge_mergeable2(MergeStrategy strategy,
341 			IndexState indexState, WorktreeState worktreeState)
342 			throws Exception {
343 		if (!validateStates(indexState, worktreeState))
344 			return;
345 
346 		BranchBuilder master = db_t.branch("master");
347 		RevCommit m0 = master.commit().add("f", "1\n2\n3\n")
348 				.message("m0")
349 				.create();
350 		RevCommit m1 = master.commit().add("f", "1-master\n2\n3\n")
351 				.message("m1").create();
352 		db_t.getRevWalk().parseCommit(m1);
353 
354 		BranchBuilder side = db_t.branch("side");
355 		RevCommit s1 = side.commit().parent(m0).add("f", "1\n2\n3-side\n")
356 				.message("s1").create();
357 		RevCommit s2 = side.commit().parent(m1)
358 				.add("f", "1-master\n2\n3-side-r\n")
359 				.message("s2(merge)")
360 				.create();
361 		RevCommit m2 = master.commit().parent(s1)
362 				.add("f", "1-master-r\n2\n3-side\n")
363 				.message("m2(merge)")
364 				.create();
365 
366 		Git git = Git.wrap(db);
367 		git.checkout().setName("master").call();
368 		modifyWorktree(worktreeState, "f", "side");
369 		modifyIndex(indexState, "f", "side");
370 
371 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
372 				worktreeState == WorktreeState.Bare);
373 		if (worktreeState != WorktreeState.Bare)
374 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
375 		try {
376 			boolean expectSuccess = true;
377 			if (!(indexState == IndexState.Bare
378 					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
379 				// index is dirty
380 				expectSuccess = false;
381 			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
382 					|| worktreeState == WorktreeState.SameAsOther)
383 				expectSuccess = false;
384 			assertEquals(Boolean.valueOf(expectSuccess),
385 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
386 			assertEquals(MergeStrategy.RECURSIVE, strategy);
387 			if (!expectSuccess)
388 				// if the merge was not successful skip testing the state of
389 				// index and workingtree
390 				return;
391 			assertEquals(
392 					"1-master-r\n2\n3-side-r",
393 					contentAsString(db, merger.getResultTreeId(), "f"));
394 			if (indexState != IndexState.Bare)
395 				assertEquals(
396 						"[f, mode:100644, content:1-master-r\n2\n3-side-r\n]",
397 						indexState(LocalDiskRepositoryTestCase.CONTENT));
398 			if (worktreeState != WorktreeState.Bare
399 					&& worktreeState != WorktreeState.Missing)
400 				assertEquals(
401 						"1-master-r\n2\n3-side-r\n",
402 						read("f"));
403 		} catch (NoMergeBaseException e) {
404 			assertEquals(MergeStrategy.RESOLVE, strategy);
405 			assertEquals(e.getReason(),
406 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
407 		}
408 	}
409 
410 	@Theory
411 	/**
412 	 * Merging m2,s2 from the following topology. m1 and s1 are not mergeable
413 	 * without conflicts. The same file is modified in both branches. The
414 	 * modifications should be mergeable but only if the merge result of
415 	 * merging m1 and s1 is choosen as parent (including the conflict markers).
416 	 *
417 	 * <pre>
418 	 * m0--m1--m2
419 	 *   \   \/
420 	 *    \  /\
421 	 *     s1--s2
422 	 * </pre>
423 	 */
424 	public void crissCrossMerge_ParentsNotMergeable(MergeStrategy strategy,
425 			IndexState indexState, WorktreeState worktreeState)
426 			throws Exception {
427 		if (!validateStates(indexState, worktreeState))
428 			return;
429 
430 		BranchBuilder master = db_t.branch("master");
431 		RevCommit m0 = master.commit().add("f", "1\n2\n3\n").message("m0")
432 				.create();
433 		RevCommit m1 = master.commit().add("f", "1\nx(master)\n2\n3\n")
434 				.message("m1").create();
435 		db_t.getRevWalk().parseCommit(m1);
436 
437 		BranchBuilder side = db_t.branch("side");
438 		RevCommit s1 = side.commit().parent(m0)
439 				.add("f", "1\nx(side)\n2\n3\ny(side)\n")
440 				.message("s1").create();
441 		RevCommit s2 = side.commit().parent(m1)
442 				.add("f", "1\nx(side)\n2\n3\ny(side-again)\n")
443 				.message("s2(merge)")
444 				.create();
445 		RevCommit m2 = master.commit().parent(s1)
446 				.add("f", "1\nx(side)\n2\n3\ny(side)\n").message("m2(merge)")
447 				.create();
448 
449 		Git git = Git.wrap(db);
450 		git.checkout().setName("master").call();
451 		modifyWorktree(worktreeState, "f", "side");
452 		modifyIndex(indexState, "f", "side");
453 
454 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
455 				worktreeState == WorktreeState.Bare);
456 		if (worktreeState != WorktreeState.Bare)
457 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
458 		try {
459 			boolean expectSuccess = true;
460 			if (!(indexState == IndexState.Bare
461 					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
462 				// index is dirty
463 				expectSuccess = false;
464 			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
465 					|| worktreeState == WorktreeState.SameAsOther)
466 				expectSuccess = false;
467 			assertEquals("Merge didn't return as expected: strategy:"
468 					+ strategy.getName() + ", indexState:" + indexState
469 					+ ", worktreeState:" + worktreeState + " . ",
470 					Boolean.valueOf(expectSuccess),
471 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
472 			assertEquals(MergeStrategy.RECURSIVE, strategy);
473 			if (!expectSuccess)
474 				// if the merge was not successful skip testing the state of
475 				// index and workingtree
476 				return;
477 			assertEquals("1\nx(side)\n2\n3\ny(side-again)",
478 					contentAsString(db, merger.getResultTreeId(), "f"));
479 			if (indexState != IndexState.Bare)
480 				assertEquals(
481 						"[f, mode:100644, content:1\nx(side)\n2\n3\ny(side-again)\n]",
482 						indexState(LocalDiskRepositoryTestCase.CONTENT));
483 			if (worktreeState != WorktreeState.Bare
484 					&& worktreeState != WorktreeState.Missing)
485 				assertEquals("1\nx(side)\n2\n3\ny(side-again)\n", read("f"));
486 		} catch (NoMergeBaseException e) {
487 			assertEquals(MergeStrategy.RESOLVE, strategy);
488 			assertEquals(e.getReason(),
489 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
490 		}
491 	}
492 
493 	@Theory
494 	/**
495 	 * Merging m2,s2 from the following topology. The same file is modified
496 	 * in both branches. The modifications should be mergeable but only if the automerge of m1 and s1
497 	 * is choosen as parent. On both branches delete and modify files untouched on the other branch.
498 	 * On both branches create new files. Make sure these files are correctly merged and
499 	 * exist in the workingtree.
500 	 *
501 	 * <pre>
502 	 * m0--m1--m2
503 	 *   \   \/
504 	 *    \  /\
505 	 *     s1--s2
506 	 * </pre>
507 	 */
508 	public void crissCrossMerge_checkOtherFiles(MergeStrategy strategy,
509 			IndexState indexState, WorktreeState worktreeState)
510 			throws Exception {
511 		if (!validateStates(indexState, worktreeState))
512 			return;
513 
514 		BranchBuilder master = db_t.branch("master");
515 		RevCommit m0 = master.commit().add("f", "1\n2\n3\n").add("m.m", "0")
516 				.add("m.d", "0").add("s.m", "0").add("s.d", "0").message("m0")
517 				.create();
518 		RevCommit m1 = master.commit().add("f", "1-master\n2\n3\n")
519 				.add("m.c", "0").add("m.m", "1").rm("m.d").message("m1")
520 				.create();
521 		db_t.getRevWalk().parseCommit(m1);
522 
523 		BranchBuilder side = db_t.branch("side");
524 		RevCommit s1 = side.commit().parent(m0).add("f", "1\n2\n3-side\n")
525 				.add("s.c", "0").add("s.m", "1").rm("s.d").message("s1")
526 				.create();
527 		RevCommit s2 = side.commit().parent(m1)
528 				.add("f", "1-master\n2\n3-side-r\n").add("m.m", "1")
529 				.add("m.c", "0").rm("m.d").message("s2(merge)").create();
530 		RevCommit m2 = master.commit().parent(s1)
531 				.add("f", "1-master-r\n2\n3-side\n").add("s.m", "1")
532 				.add("s.c", "0").rm("s.d").message("m2(merge)").create();
533 
534 		Git git = Git.wrap(db);
535 		git.checkout().setName("master").call();
536 		modifyWorktree(worktreeState, "f", "side");
537 		modifyIndex(indexState, "f", "side");
538 
539 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
540 				worktreeState == WorktreeState.Bare);
541 		if (worktreeState != WorktreeState.Bare)
542 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
543 		try {
544 			boolean expectSuccess = true;
545 			if (!(indexState == IndexState.Bare
546 					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
547 				// index is dirty
548 				expectSuccess = false;
549 			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
550 					|| worktreeState == WorktreeState.SameAsOther)
551 				expectSuccess = false;
552 			assertEquals(Boolean.valueOf(expectSuccess),
553 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
554 			assertEquals(MergeStrategy.RECURSIVE, strategy);
555 			if (!expectSuccess)
556 				// if the merge was not successful skip testing the state of
557 				// index and workingtree
558 				return;
559 			assertEquals(
560 					"1-master-r\n2\n3-side-r",
561 					contentAsString(db, merger.getResultTreeId(), "f"));
562 			if (indexState != IndexState.Bare)
563 				assertEquals(
564 						"[f, mode:100644, content:1-master-r\n2\n3-side-r\n][m.c, mode:100644, content:0][m.m, mode:100644, content:1][s.c, mode:100644, content:0][s.m, mode:100644, content:1]",
565 						indexState(LocalDiskRepositoryTestCase.CONTENT));
566 			if (worktreeState != WorktreeState.Bare
567 					&& worktreeState != WorktreeState.Missing) {
568 				assertEquals(
569 						"1-master-r\n2\n3-side-r\n",
570 						read("f"));
571 				assertTrue(check("s.c"));
572 				assertFalse(check("s.d"));
573 				assertTrue(check("s.m"));
574 				assertTrue(check("m.c"));
575 				assertFalse(check("m.d"));
576 				assertTrue(check("m.m"));
577 			}
578 		} catch (NoMergeBaseException e) {
579 			assertEquals(MergeStrategy.RESOLVE, strategy);
580 			assertEquals(e.getReason(),
581 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
582 		}
583 	}
584 
585 	@Theory
586 	/**
587 	 * Merging m2,s2 from the following topology. The same file is modified
588 	 * in both branches. The modifications are not automatically
589 	 * mergeable. m2 and s2 contain branch specific conflict resolutions.
590 	 * Therefore m2 and s2 don't contain the same content.
591 	 *
592 	 * <pre>
593 	 * m0--m1--m2
594 	 *   \   \/
595 	 *    \  /\
596 	 *     s1--s2
597 	 * </pre>
598 	 */
599 	public void crissCrossMerge_nonmergeable(MergeStrategy strategy,
600 			IndexState indexState, WorktreeState worktreeState)
601 			throws Exception {
602 		if (!validateStates(indexState, worktreeState))
603 			return;
604 
605 		BranchBuilder master = db_t.branch("master");
606 		RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
607 				.message("m0").create();
608 		RevCommit m1 = master.commit()
609 				.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1")
610 				.create();
611 		db_t.getRevWalk().parseCommit(m1);
612 
613 		BranchBuilder side = db_t.branch("side");
614 		RevCommit s1 = side.commit().parent(m0)
615 				.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1")
616 				.create();
617 		RevCommit s2 = side.commit().parent(m1)
618 				.add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n")
619 				.message("s2(merge)").create();
620 		RevCommit m2 = master.commit().parent(s1)
621 				.add("f", "1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n")
622 				.message("m2(merge)").create();
623 
624 		Git git = Git.wrap(db);
625 		git.checkout().setName("master").call();
626 		modifyWorktree(worktreeState, "f", "side");
627 		modifyIndex(indexState, "f", "side");
628 
629 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
630 				worktreeState == WorktreeState.Bare);
631 		if (worktreeState != WorktreeState.Bare)
632 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
633 		try {
634 			assertFalse(merger.merge(new RevCommit[] { m2, s2 }));
635 			assertEquals(MergeStrategy.RECURSIVE, strategy);
636 			if (indexState == IndexState.SameAsHead
637 					&& worktreeState == WorktreeState.SameAsHead) {
638 				assertEquals(
639 						"[f, mode:100644, stage:1, content:1-master\n2\n3\n4\n5\n6\n7\n8\n9-side\n]"
640 								+ "[f, mode:100644, stage:2, content:1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n]"
641 								+ "[f, mode:100644, stage:3, content:1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n]",
642 						indexState(LocalDiskRepositoryTestCase.CONTENT));
643 				assertEquals(
644 						"1-master\n2\n3\n4\n5\n6\n<<<<<<< OURS\n7-conflict\n=======\n7-res(side)\n>>>>>>> THEIRS\n8\n9-side\n",
645 						read("f"));
646 			}
647 		} catch (NoMergeBaseException e) {
648 			assertEquals(MergeStrategy.RESOLVE, strategy);
649 			assertEquals(e.getReason(),
650 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
651 		}
652 	}
653 
654 	@Theory
655 	/**
656 	 * Merging m2,s2 which have three common predecessors.The same file is modified
657 	 * in all branches. The modifications should be mergeable. m2 and s2
658 	 * contain branch specific conflict resolutions. Therefore m2 and s2
659 	 * don't contain the same content.
660 	 *
661 	 * <pre>
662 	 *     m1-----m2
663 	 *    /  \/  /
664 	 *   /   /\ /
665 	 * m0--o1  x
666 	 *   \   \/ \
667 	 *    \  /\  \
668 	 *     s1-----s2
669 	 * </pre>
670 	 */
671 	public void crissCrossMerge_ThreeCommonPredecessors(MergeStrategy strategy,
672 			IndexState indexState, WorktreeState worktreeState)
673 			throws Exception {
674 		if (!validateStates(indexState, worktreeState))
675 			return;
676 
677 		BranchBuilder master = db_t.branch("master");
678 		RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
679 				.message("m0").create();
680 		RevCommit m1 = master.commit()
681 				.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1")
682 				.create();
683 		BranchBuilder side = db_t.branch("side");
684 		RevCommit s1 = side.commit().parent(m0)
685 				.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1")
686 				.create();
687 		BranchBuilder other = db_t.branch("other");
688 		RevCommit o1 = other.commit().parent(m0)
689 				.add("f", "1\n2\n3\n4\n5-other\n6\n7\n8\n9\n").message("o1")
690 				.create();
691 
692 		RevCommit m2 = master
693 				.commit()
694 				.parent(s1)
695 				.parent(o1)
696 				.add("f",
697 						"1-master\n2\n3-res(master)\n4\n5-other\n6\n7\n8\n9-side\n")
698 				.message("m2(merge)").create();
699 
700 		RevCommit s2 = side
701 				.commit()
702 				.parent(m1)
703 				.parent(o1)
704 				.add("f",
705 						"1-master\n2\n3\n4\n5-other\n6\n7-res(side)\n8\n9-side\n")
706 				.message("s2(merge)").create();
707 
708 		Git git = Git.wrap(db);
709 		git.checkout().setName("master").call();
710 		modifyWorktree(worktreeState, "f", "side");
711 		modifyIndex(indexState, "f", "side");
712 
713 		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
714 				worktreeState == WorktreeState.Bare);
715 		if (worktreeState != WorktreeState.Bare)
716 			merger.setWorkingTreeIterator(new FileTreeIterator(db));
717 		try {
718 			boolean expectSuccess = true;
719 			if (!(indexState == IndexState.Bare
720 					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
721 				// index is dirty
722 				expectSuccess = false;
723 			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
724 					|| worktreeState == WorktreeState.SameAsOther)
725 				// workingtree is dirty
726 				expectSuccess = false;
727 
728 			assertEquals(Boolean.valueOf(expectSuccess),
729 					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
730 			assertEquals(MergeStrategy.RECURSIVE, strategy);
731 			if (!expectSuccess)
732 				// if the merge was not successful skip testing the state of index and workingtree
733 				return;
734 			assertEquals(
735 					"1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side",
736 					contentAsString(db, merger.getResultTreeId(), "f"));
737 			if (indexState != IndexState.Bare)
738 				assertEquals(
739 						"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n]",
740 						indexState(LocalDiskRepositoryTestCase.CONTENT));
741 			if (worktreeState != WorktreeState.Bare
742 					&& worktreeState != WorktreeState.Missing)
743 				assertEquals(
744 						"1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n",
745 						read("f"));
746 		} catch (NoMergeBaseException e) {
747 			assertEquals(MergeStrategy.RESOLVE, strategy);
748 			assertEquals(e.getReason(),
749 					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
750 		}
751 	}
752 
753 	void modifyIndex(IndexState indexState, String path, String other)
754 			throws Exception {
755 		RevBlob blob;
756 		switch (indexState) {
757 		case Missing:
758 			setIndex(null, path);
759 			break;
760 		case SameAsHead:
761 			setIndex(contentId(Constants.HEAD, path), path);
762 			break;
763 		case SameAsOther:
764 			setIndex(contentId(other, path), path);
765 			break;
766 		case SameAsWorkTree:
767 			blob = db_t.blob(read(path));
768 			setIndex(blob, path);
769 			break;
770 		case DifferentFromHeadAndOtherAndWorktree:
771 			blob = db_t.blob(Integer.toString(counter++));
772 			setIndex(blob, path);
773 			break;
774 		case Bare:
775 			File file = new File(db.getDirectory(), "index");
776 			if (!file.exists())
777 				return;
778 			db.close();
779 			file.delete();
780 			db = new FileRepository(db.getDirectory());
781 			db_t = new TestRepository<>(db);
782 			break;
783 		}
784 	}
785 
786 	private void setIndex(ObjectId id, String path)
787 			throws MissingObjectException, IOException {
788 		DirCache lockedDircache;
789 		DirCacheEditor dcedit;
790 
791 		lockedDircache = db.lockDirCache();
792 		dcedit = lockedDircache.editor();
793 		try {
794 			if (id != null) {
795 				final ObjectLoader contLoader = db.newObjectReader().open(id);
796 				dcedit.add(new DirCacheEditor.PathEdit(path) {
797 					@Override
798 					public void apply(DirCacheEntry ent) {
799 						ent.setFileMode(FileMode.REGULAR_FILE);
800 						ent.setLength(contLoader.getSize());
801 						ent.setObjectId(id);
802 					}
803 				});
804 			} else
805 				dcedit.add(new DirCacheEditor.DeletePath(path));
806 		} finally {
807 			dcedit.commit();
808 		}
809 	}
810 
811 	private ObjectId contentId(String revName, String path) throws Exception {
812 		RevCommit headCommit = db_t.getRevWalk().parseCommit(
813 				db.resolve(revName));
814 		db_t.parseBody(headCommit);
815 		return db_t.get(headCommit.getTree(), path).getId();
816 	}
817 
818 	void modifyWorktree(WorktreeState worktreeState, String path, String other)
819 			throws Exception {
820 		switch (worktreeState) {
821 		case Missing:
822 			new File(db.getWorkTree(), path).delete();
823 			break;
824 		case DifferentFromHeadAndOther:
825 			write(new File(db.getWorkTree(), path),
826 					Integer.toString(counter++));
827 			break;
828 		case SameAsHead:
829 			try (FileOutputStream fos = new FileOutputStream(
830 					new File(db.getWorkTree(), path))) {
831 				db.newObjectReader().open(contentId(Constants.HEAD, path))
832 						.copyTo(fos);
833 			}
834 			break;
835 		case SameAsOther:
836 			try (FileOutputStream fos = new FileOutputStream(
837 					new File(db.getWorkTree(), path))) {
838 				db.newObjectReader().open(contentId(other, path)).copyTo(fos);
839 			}
840 			break;
841 		case Bare:
842 			if (db.isBare())
843 				return;
844 			File workTreeFile = db.getWorkTree();
845 			db.getConfig().setBoolean("core", null, "bare", true);
846 			db.getDirectory().renameTo(new File(workTreeFile, "test.git"));
847 			db = new FileRepository(new File(workTreeFile, "test.git"));
848 			db_t = new TestRepository<>(db);
849 		}
850 	}
851 
852 	private boolean validateStates(IndexState indexState,
853 			WorktreeState worktreeState) {
854 		if (worktreeState == WorktreeState.Bare
855 				&& indexState != IndexState.Bare)
856 			return false;
857 		if (worktreeState != WorktreeState.Bare
858 				&& indexState == IndexState.Bare)
859 			return false;
860 		if (worktreeState != WorktreeState.DifferentFromHeadAndOther
861 				&& indexState == IndexState.SameAsWorkTree)
862 			// would be a duplicate: the combination WorktreeState.X and
863 			// IndexState.X already covered this
864 			return false;
865 		return true;
866 	}
867 
868 	private String contentAsString(Repository r, ObjectId treeId, String path)
869 			throws MissingObjectException, IOException {
870 		AnyObjectId blobId;
871 		try (TreeWalk tw = new TreeWalk(r)) {
872 			tw.addTree(treeId);
873 			tw.setFilter(PathFilter.create(path));
874 			tw.setRecursive(true);
875 			if (!tw.next()) {
876 				return null;
877 			}
878 			blobId = tw.getObjectId(0);
879 		}
880 
881 		StringBuilder result = new StringBuilder();
882 		ObjectReader or = r.newObjectReader();
883 		try (BufferedReader br = new BufferedReader(
884 				new InputStreamReader(or.open(blobId).openStream()))) {
885 			String line;
886 			boolean first = true;
887 			while ((line = br.readLine()) != null) {
888 				if (!first) {
889 					result.append('\n');
890 				}
891 				result.append(line);
892 				first = false;
893 			}
894 			return result.toString();
895 		}
896 	}
897 }