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