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