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