Browse Source
Extend ResolveMerger with RecursiveMerger to merge two tips that have up to 200 bases. Bug: 380314 CQ: 6854 Change-Id: I6292bb7bda55c0242a448a94956f2d6a94fddbaa Also-by: Christian Halstrick <christian.halstrick@sap.com> Signed-off-by: Chris Aniszczyk <zx@twitter.com> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>stable-3.0
George C. Young
12 years ago
committed by
Matthias Sohn
12 changed files with 1301 additions and 94 deletions
@ -0,0 +1,578 @@
|
||||
/* |
||||
* Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com> |
||||
* and other copyright owners as documented in the project's IP log. |
||||
* |
||||
* This program and the accompanying materials are made available |
||||
* under the terms of the Eclipse Distribution License v1.0 which |
||||
* accompanies this distribution, is reproduced below, and is |
||||
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||
* |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or |
||||
* without modification, are permitted provided that the following |
||||
* conditions are met: |
||||
* |
||||
* - Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* - Redistributions in binary form must reproduce the above |
||||
* copyright notice, this list of conditions and the following |
||||
* disclaimer in the documentation and/or other materials provided |
||||
* with the distribution. |
||||
* |
||||
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||
* names of its contributors may be used to endorse or promote |
||||
* products derived from this software without specific prior |
||||
* written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package org.eclipse.jgit.merge; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertFalse; |
||||
|
||||
import java.io.BufferedReader; |
||||
import java.io.File; |
||||
import java.io.FileOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStreamReader; |
||||
|
||||
import org.eclipse.jgit.api.Git; |
||||
import org.eclipse.jgit.dircache.DirCache; |
||||
import org.eclipse.jgit.dircache.DirCacheEditor; |
||||
import org.eclipse.jgit.dircache.DirCacheEntry; |
||||
import org.eclipse.jgit.errors.MissingObjectException; |
||||
import org.eclipse.jgit.errors.NoMergeBaseException; |
||||
import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; |
||||
import org.eclipse.jgit.junit.RepositoryTestCase; |
||||
import org.eclipse.jgit.junit.TestRepository; |
||||
import org.eclipse.jgit.junit.TestRepository.BranchBuilder; |
||||
import org.eclipse.jgit.lib.AnyObjectId; |
||||
import org.eclipse.jgit.lib.Constants; |
||||
import org.eclipse.jgit.lib.FileMode; |
||||
import org.eclipse.jgit.lib.ObjectId; |
||||
import org.eclipse.jgit.lib.ObjectLoader; |
||||
import org.eclipse.jgit.lib.ObjectReader; |
||||
import org.eclipse.jgit.lib.Repository; |
||||
import org.eclipse.jgit.revwalk.RevBlob; |
||||
import org.eclipse.jgit.revwalk.RevCommit; |
||||
import org.eclipse.jgit.storage.file.FileRepository; |
||||
import org.eclipse.jgit.treewalk.FileTreeIterator; |
||||
import org.eclipse.jgit.treewalk.TreeWalk; |
||||
import org.eclipse.jgit.treewalk.filter.PathFilter; |
||||
import org.junit.Before; |
||||
import org.junit.experimental.theories.DataPoints; |
||||
import org.junit.experimental.theories.Theories; |
||||
import org.junit.experimental.theories.Theory; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
@RunWith(Theories.class) |
||||
public class RecursiveMergerTest extends RepositoryTestCase { |
||||
static int counter = 0; |
||||
|
||||
@DataPoints |
||||
public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] { |
||||
MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE }; |
||||
|
||||
public enum IndexState { |
||||
Bare, Missing, SameAsHead, SameAsOther, SameAsWorkTree, DifferentFromHeadAndOtherAndWorktree |
||||
} |
||||
|
||||
@DataPoints |
||||
public static IndexState[] indexStates = IndexState.values(); |
||||
|
||||
public enum WorktreeState { |
||||
Bare, Missing, SameAsHead, DifferentFromHeadAndOther, SameAsOther; |
||||
} |
||||
|
||||
@DataPoints |
||||
public static WorktreeState[] worktreeStates = WorktreeState.values(); |
||||
|
||||
private TestRepository<FileRepository> db_t; |
||||
|
||||
@Override |
||||
@Before |
||||
public void setUp() throws Exception { |
||||
super.setUp(); |
||||
db_t = new TestRepository<FileRepository>(db); |
||||
} |
||||
|
||||
@Theory |
||||
/** |
||||
* Merging m2,s2 from the following topology. In master and side different |
||||
* files are touched. No need to do a real content merge. |
||||
* |
||||
* <pre> |
||||
* m0--m1--m2 |
||||
* \ \/ |
||||
* \ /\ |
||||
* s1--s2 |
||||
* </pre> |
||||
*/ |
||||
public void crissCrossMerge(MergeStrategy strategy, IndexState indexState, |
||||
WorktreeState worktreeState) throws Exception { |
||||
if (!validateStates(indexState, worktreeState)) |
||||
return; |
||||
// fill the repo
|
||||
BranchBuilder master = db_t.branch("master"); |
||||
RevCommit m0 = master.commit().add("m", ",m0").message("m0").create(); |
||||
RevCommit m1 = master.commit().add("m", "m1").message("m1").create(); |
||||
db_t.getRevWalk().parseCommit(m1); |
||||
|
||||
BranchBuilder side = db_t.branch("side"); |
||||
RevCommit s1 = side.commit().parent(m0).add("s", "s1").message("s1") |
||||
.create(); |
||||
RevCommit s2 = side.commit().parent(m1).add("m", "m1") |
||||
.message("s2(merge)").create(); |
||||
RevCommit m2 = master.commit().parent(s1).add("s", "s1") |
||||
.message("m2(merge)").create(); |
||||
|
||||
Git git = Git.wrap(db); |
||||
git.checkout().setName("master").call(); |
||||
modifyWorktree(worktreeState, "m", "side"); |
||||
modifyWorktree(worktreeState, "s", "side"); |
||||
modifyIndex(indexState, "m", "side"); |
||||
modifyIndex(indexState, "s", "side"); |
||||
|
||||
ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, |
||||
worktreeState == WorktreeState.Bare); |
||||
if (worktreeState != WorktreeState.Bare) |
||||
merger.setWorkingTreeIterator(new FileTreeIterator(db)); |
||||
try { |
||||
boolean expectSuccess = true; |
||||
if (!(indexState == IndexState.Bare |
||||
|| indexState == IndexState.Missing |
||||
|| indexState == IndexState.SameAsHead || indexState == IndexState.SameAsOther)) |
||||
// index is dirty
|
||||
expectSuccess = false; |
||||
|
||||
assertEquals(Boolean.valueOf(expectSuccess), |
||||
Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); |
||||
assertEquals(MergeStrategy.RECURSIVE, strategy); |
||||
assertEquals("m1", |
||||
contentAsString(db, merger.getResultTreeId(), "m")); |
||||
assertEquals("s1", |
||||
contentAsString(db, merger.getResultTreeId(), "s")); |
||||
} catch (NoMergeBaseException e) { |
||||
assertEquals(MergeStrategy.RESOLVE, strategy); |
||||
assertEquals(e.getReason(), |
||||
MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); |
||||
} |
||||
} |
||||
|
||||
@Theory |
||||
/** |
||||
* Merging m2,s2 from the following topology. The same file is modified |
||||
* in both branches. The modifications should be mergeable. m2 and s2 |
||||
* contain branch specific conflict resolutions. Therefore m2 and don't contain the same content. |
||||
* |
||||
* <pre> |
||||
* m0--m1--m2 |
||||
* \ \/ |
||||
* \ /\ |
||||
* s1--s2 |
||||
* </pre> |
||||
*/ |
||||
public void crissCrossMerge_mergeable(MergeStrategy strategy, |
||||
IndexState indexState, WorktreeState worktreeState) |
||||
throws Exception { |
||||
if (!validateStates(indexState, worktreeState)) |
||||
return; |
||||
|
||||
BranchBuilder master = db_t.branch("master"); |
||||
RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n") |
||||
.message("m0").create(); |
||||
RevCommit m1 = master.commit() |
||||
.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1") |
||||
.create(); |
||||
db_t.getRevWalk().parseCommit(m1); |
||||
|
||||
BranchBuilder side = db_t.branch("side"); |
||||
RevCommit s1 = side.commit().parent(m0) |
||||
.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1") |
||||
.create(); |
||||
RevCommit s2 = side.commit().parent(m1) |
||||
.add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n") |
||||
.message("s2(merge)").create(); |
||||
RevCommit m2 = master |
||||
.commit() |
||||
.parent(s1) |
||||
.add("f", "1-master\n2\n3-res(master)\n4\n5\n6\n7\n8\n9-side\n") |
||||
.message("m2(merge)").create(); |
||||
|
||||
Git git = Git.wrap(db); |
||||
git.checkout().setName("master").call(); |
||||
modifyWorktree(worktreeState, "f", "side"); |
||||
modifyIndex(indexState, "f", "side"); |
||||
|
||||
ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, |
||||
worktreeState == WorktreeState.Bare); |
||||
if (worktreeState != WorktreeState.Bare) |
||||
merger.setWorkingTreeIterator(new FileTreeIterator(db)); |
||||
try { |
||||
boolean expectSuccess = true; |
||||
if (!(indexState == IndexState.Bare |
||||
|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead)) |
||||
// index is dirty
|
||||
expectSuccess = false; |
||||
else if (worktreeState == WorktreeState.DifferentFromHeadAndOther |
||||
|| worktreeState == WorktreeState.SameAsOther) |
||||
expectSuccess = false; |
||||
assertEquals(Boolean.valueOf(expectSuccess), |
||||
Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); |
||||
assertEquals(MergeStrategy.RECURSIVE, strategy); |
||||
if (!expectSuccess) |
||||
// if the merge was not successful skip testing the state of index and workingtree
|
||||
return; |
||||
assertEquals( |
||||
"1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side", |
||||
contentAsString(db, merger.getResultTreeId(), "f")); |
||||
if (indexState != IndexState.Bare) |
||||
assertEquals( |
||||
"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n]", |
||||
indexState(RepositoryTestCase.CONTENT)); |
||||
if (worktreeState != WorktreeState.Bare |
||||
&& worktreeState != WorktreeState.Missing) |
||||
assertEquals( |
||||
"1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n", |
||||
read("f")); |
||||
} catch (NoMergeBaseException e) { |
||||
assertEquals(MergeStrategy.RESOLVE, strategy); |
||||
assertEquals(e.getReason(), |
||||
MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); |
||||
} |
||||
} |
||||
|
||||
@Theory |
||||
/** |
||||
* Merging m2,s2 from the following topology. The same file is modified |
||||
* in both branches. The modifications are not automatically |
||||
* mergeable. m2 and s2 contain branch specific conflict resolutions. |
||||
* Therefore m2 and s2 don't contain the same content. |
||||
* |
||||
* <pre> |
||||
* m0--m1--m2 |
||||
* \ \/ |
||||
* \ /\ |
||||
* s1--s2 |
||||
* </pre> |
||||
*/ |
||||
public void crissCrossMerge_nonmergeable(MergeStrategy strategy, |
||||
IndexState indexState, WorktreeState worktreeState) |
||||
throws Exception { |
||||
if (!validateStates(indexState, worktreeState)) |
||||
return; |
||||
|
||||
BranchBuilder master = db_t.branch("master"); |
||||
RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n") |
||||
.message("m0").create(); |
||||
RevCommit m1 = master.commit() |
||||
.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1") |
||||
.create(); |
||||
db_t.getRevWalk().parseCommit(m1); |
||||
|
||||
BranchBuilder side = db_t.branch("side"); |
||||
RevCommit s1 = side.commit().parent(m0) |
||||
.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1") |
||||
.create(); |
||||
RevCommit s2 = side.commit().parent(m1) |
||||
.add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n") |
||||
.message("s2(merge)").create(); |
||||
RevCommit m2 = master.commit().parent(s1) |
||||
.add("f", "1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n") |
||||
.message("m2(merge)").create(); |
||||
|
||||
Git git = Git.wrap(db); |
||||
git.checkout().setName("master").call(); |
||||
modifyWorktree(worktreeState, "f", "side"); |
||||
modifyIndex(indexState, "f", "side"); |
||||
|
||||
ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, |
||||
worktreeState == WorktreeState.Bare); |
||||
if (worktreeState != WorktreeState.Bare) |
||||
merger.setWorkingTreeIterator(new FileTreeIterator(db)); |
||||
try { |
||||
assertFalse(merger.merge(new RevCommit[] { m2, s2 })); |
||||
assertEquals(MergeStrategy.RECURSIVE, strategy); |
||||
if (indexState == IndexState.SameAsHead |
||||
&& worktreeState == WorktreeState.SameAsHead) { |
||||
assertEquals( |
||||
"[f, mode:100644, stage:1, content:1-master\n2\n3\n4\n5\n6\n7\n8\n9-side\n]" |
||||
+ "[f, mode:100644, stage:2, content:1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n]" |
||||
+ "[f, mode:100644, stage:3, content:1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n]", |
||||
indexState(RepositoryTestCase.CONTENT)); |
||||
assertEquals( |
||||
"1-master\n2\n3\n4\n5\n6\n<<<<<<< OURS\n7-conflict\n=======\n7-res(side)\n>>>>>>> THEIRS\n8\n9-side\n", |
||||
read("f")); |
||||
} |
||||
} catch (NoMergeBaseException e) { |
||||
assertEquals(MergeStrategy.RESOLVE, strategy); |
||||
assertEquals(e.getReason(), |
||||
MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); |
||||
} |
||||
} |
||||
|
||||
@Theory |
||||
/** |
||||
* Merging m2,s2 which have three common predecessors.The same file is modified |
||||
* in all branches. The modifications should be mergeable. m2 and s2 |
||||
* contain branch specific conflict resolutions. Therefore m2 and s2 |
||||
* don't contain the same content. |
||||
* |
||||
* <pre> |
||||
* m1-----m2 |
||||
* / \/ / |
||||
* / /\ / |
||||
* m0--o1 x |
||||
* \ \/ \ |
||||
* \ /\ \ |
||||
* s1-----s2 |
||||
* </pre> |
||||
*/ |
||||
public void crissCrossMerge_ThreeCommonPredecessors(MergeStrategy strategy, |
||||
IndexState indexState, WorktreeState worktreeState) |
||||
throws Exception { |
||||
if (!validateStates(indexState, worktreeState)) |
||||
return; |
||||
|
||||
BranchBuilder master = db_t.branch("master"); |
||||
RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n") |
||||
.message("m0").create(); |
||||
RevCommit m1 = master.commit() |
||||
.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1") |
||||
.create(); |
||||
BranchBuilder side = db_t.branch("side"); |
||||
RevCommit s1 = side.commit().parent(m0) |
||||
.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1") |
||||
.create(); |
||||
BranchBuilder other = db_t.branch("other"); |
||||
RevCommit o1 = other.commit().parent(m0) |
||||
.add("f", "1\n2\n3\n4\n5-other\n6\n7\n8\n9\n").message("o1") |
||||
.create(); |
||||
|
||||
RevCommit m2 = master |
||||
.commit() |
||||
.parent(s1) |
||||
.parent(o1) |
||||
.add("f", |
||||
"1-master\n2\n3-res(master)\n4\n5-other\n6\n7\n8\n9-side\n") |
||||
.message("m2(merge)").create(); |
||||
|
||||
RevCommit s2 = side |
||||
.commit() |
||||
.parent(m1) |
||||
.parent(o1) |
||||
.add("f", |
||||
"1-master\n2\n3\n4\n5-other\n6\n7-res(side)\n8\n9-side\n") |
||||
.message("s2(merge)").create(); |
||||
|
||||
Git git = Git.wrap(db); |
||||
git.checkout().setName("master").call(); |
||||
modifyWorktree(worktreeState, "f", "side"); |
||||
modifyIndex(indexState, "f", "side"); |
||||
|
||||
ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, |
||||
worktreeState == WorktreeState.Bare); |
||||
if (worktreeState != WorktreeState.Bare) |
||||
merger.setWorkingTreeIterator(new FileTreeIterator(db)); |
||||
try { |
||||
boolean expectSuccess = true; |
||||
if (!(indexState == IndexState.Bare |
||||
|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead)) |
||||
// index is dirty
|
||||
expectSuccess = false; |
||||
else if (worktreeState == WorktreeState.DifferentFromHeadAndOther |
||||
|| worktreeState == WorktreeState.SameAsOther) |
||||
// workingtree is dirty
|
||||
expectSuccess = false; |
||||
|
||||
assertEquals(Boolean.valueOf(expectSuccess), |
||||
Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); |
||||
assertEquals(MergeStrategy.RECURSIVE, strategy); |
||||
if (!expectSuccess) |
||||
// if the merge was not successful skip testing the state of index and workingtree
|
||||
return; |
||||
assertEquals( |
||||
"1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side", |
||||
contentAsString(db, merger.getResultTreeId(), "f")); |
||||
if (indexState != IndexState.Bare) |
||||
assertEquals( |
||||
"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n]", |
||||
indexState(RepositoryTestCase.CONTENT)); |
||||
if (worktreeState != WorktreeState.Bare |
||||
&& worktreeState != WorktreeState.Missing) |
||||
assertEquals( |
||||
"1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n", |
||||
read("f")); |
||||
} catch (NoMergeBaseException e) { |
||||
assertEquals(MergeStrategy.RESOLVE, strategy); |
||||
assertEquals(e.getReason(), |
||||
MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); |
||||
} |
||||
} |
||||
|
||||
void modifyIndex(IndexState indexState, String path, String other) |
||||
throws Exception { |
||||
RevBlob blob; |
||||
switch (indexState) { |
||||
case Missing: |
||||
setIndex(null, path); |
||||
break; |
||||
case SameAsHead: |
||||
setIndex(contentId(Constants.HEAD, path), path); |
||||
break; |
||||
case SameAsOther: |
||||
setIndex(contentId(other, path), path); |
||||
break; |
||||
case SameAsWorkTree: |
||||
blob = db_t.blob(read(path)); |
||||
setIndex(blob, path); |
||||
break; |
||||
case DifferentFromHeadAndOtherAndWorktree: |
||||
blob = db_t.blob(Integer.toString(counter++)); |
||||
setIndex(blob, path); |
||||
break; |
||||
case Bare: |
||||
File file = new File(db.getDirectory(), "index"); |
||||
if (!file.exists()) |
||||
return; |
||||
db.close(); |
||||
file.delete(); |
||||
db = new FileRepository(db.getDirectory()); |
||||
db_t = new TestRepository<FileRepository>(db); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
private void setIndex(final ObjectId id, String path) |
||||
throws MissingObjectException, IOException { |
||||
DirCache lockedDircache; |
||||
DirCacheEditor dcedit; |
||||
|
||||
lockedDircache = db.lockDirCache(); |
||||
dcedit = lockedDircache.editor(); |
||||
try { |
||||
if (id != null) { |
||||
final ObjectLoader contLoader = db.newObjectReader().open(id); |
||||
dcedit.add(new DirCacheEditor.PathEdit(path) { |
||||
@Override |
||||
public void apply(DirCacheEntry ent) { |
||||
ent.setFileMode(FileMode.REGULAR_FILE); |
||||
ent.setLength(contLoader.getSize()); |
||||
ent.setObjectId(id); |
||||
} |
||||
}); |
||||
} else |
||||
dcedit.add(new DirCacheEditor.DeletePath(path)); |
||||
} finally { |
||||
dcedit.commit(); |
||||
} |
||||
} |
||||
|
||||
private ObjectId contentId(String revName, String path) throws Exception { |
||||
RevCommit headCommit = db_t.getRevWalk().parseCommit( |
||||
db.resolve(revName)); |
||||
db_t.parseBody(headCommit); |
||||
return db_t.get(headCommit.getTree(), path).getId(); |
||||
} |
||||
|
||||
void modifyWorktree(WorktreeState worktreeState, String path, String other) |
||||
throws Exception { |
||||
FileOutputStream fos = null; |
||||
ObjectId bloblId; |
||||
|
||||
try { |
||||
switch (worktreeState) { |
||||
case Missing: |
||||
new File(db.getWorkTree(), path).delete(); |
||||
break; |
||||
case DifferentFromHeadAndOther: |
||||
write(new File(db.getWorkTree(), path), |
||||
Integer.toString(counter++)); |
||||
break; |
||||
case SameAsHead: |
||||
bloblId = contentId(Constants.HEAD, path); |
||||
fos = new FileOutputStream(new File(db.getWorkTree(), path)); |
||||
db.newObjectReader().open(bloblId).copyTo(fos); |
||||
break; |
||||
case SameAsOther: |
||||
bloblId = contentId(other, path); |
||||
fos = new FileOutputStream(new File(db.getWorkTree(), path)); |
||||
db.newObjectReader().open(bloblId).copyTo(fos); |
||||
break; |
||||
case Bare: |
||||
if (db.isBare()) |
||||
return; |
||||
File workTreeFile = db.getWorkTree(); |
||||
db.getConfig().setBoolean("core", null, "bare", true); |
||||
db.getDirectory().renameTo(new File(workTreeFile, "test.git")); |
||||
db = new FileRepository(new File(workTreeFile, "test.git")); |
||||
db_t = new TestRepository<FileRepository>(db); |
||||
} |
||||
} finally { |
||||
if (fos != null) |
||||
fos.close(); |
||||
} |
||||
} |
||||
|
||||
private boolean validateStates(IndexState indexState, |
||||
WorktreeState worktreeState) { |
||||
if (worktreeState == WorktreeState.Bare |
||||
&& indexState != IndexState.Bare) |
||||
return false; |
||||
if (worktreeState != WorktreeState.Bare |
||||
&& indexState == IndexState.Bare) |
||||
return false; |
||||
if (worktreeState != WorktreeState.DifferentFromHeadAndOther |
||||
&& indexState == IndexState.SameAsWorkTree) |
||||
// would be a duplicate: the combination WorktreeState.X and
|
||||
// IndexState.X already covered this
|
||||
return false; |
||||
return true; |
||||
} |
||||
|
||||
private String contentAsString(Repository r, ObjectId treeId, String path) |
||||
throws MissingObjectException, IOException { |
||||
TreeWalk tw = new TreeWalk(r); |
||||
tw.addTree(treeId); |
||||
tw.setFilter(PathFilter.create(path)); |
||||
tw.setRecursive(true); |
||||
if (!tw.next()) |
||||
return null; |
||||
AnyObjectId blobId = tw.getObjectId(0); |
||||
|
||||
StringBuilder result = new StringBuilder(); |
||||
BufferedReader br = null; |
||||
ObjectReader or = r.newObjectReader(); |
||||
try { |
||||
br = new BufferedReader(new InputStreamReader(or.open(blobId) |
||||
.openStream())); |
||||
String line; |
||||
boolean first = true; |
||||
while ((line = br.readLine()) != null) { |
||||
if (!first) |
||||
result.append('\n'); |
||||
result.append(line); |
||||
first = false; |
||||
} |
||||
return result.toString(); |
||||
} finally { |
||||
if (br != null) |
||||
br.close(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,124 @@
|
||||
/* |
||||
* Copyright (C) 2013, Christian Halstrick <christian.halstrick@sap.com> |
||||
* and other copyright owners as documented in the project's IP log. |
||||
* |
||||
* This program and the accompanying materials are made available |
||||
* under the terms of the Eclipse Distribution License v1.0 which |
||||
* accompanies this distribution, is reproduced below, and is |
||||
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||
* |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or |
||||
* without modification, are permitted provided that the following |
||||
* conditions are met: |
||||
* |
||||
* - Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* - Redistributions in binary form must reproduce the above |
||||
* copyright notice, this list of conditions and the following |
||||
* disclaimer in the documentation and/or other materials provided |
||||
* with the distribution. |
||||
* |
||||
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||
* names of its contributors may be used to endorse or promote |
||||
* products derived from this software without specific prior |
||||
* written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package org.eclipse.jgit.errors; |
||||
|
||||
import java.io.IOException; |
||||
import java.text.MessageFormat; |
||||
|
||||
import org.eclipse.jgit.internal.JGitText; |
||||
import org.eclipse.jgit.merge.RecursiveMerger; |
||||
|
||||
/** |
||||
* Exception thrown if a merge fails because no merge base could be determined. |
||||
*/ |
||||
public class NoMergeBaseException extends IOException { |
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
private MergeBaseFailureReason reason; |
||||
|
||||
/** |
||||
* An enum listing the different reason why no merge base could be |
||||
* determined. |
||||
*/ |
||||
public static enum MergeBaseFailureReason { |
||||
/** |
||||
* Multiple merge bases have been found (e.g. the commits to be merged |
||||
* have multiple common predecessors) but the merge strategy doesn't |
||||
* support this (e.g. ResolveMerge) |
||||
*/ |
||||
MULTIPLE_MERGE_BASES_NOT_SUPPORTED, |
||||
|
||||
/** |
||||
* The number of merge bases exceeds {@link RecursiveMerger#MAX_BASES} |
||||
*/ |
||||
TOO_MANY_MERGE_BASES, |
||||
|
||||
/** |
||||
* In order to find a single merge base it may required to merge |
||||
* together multiple common predecessors. If during these merges |
||||
* conflicts occur the merge fails with this reason |
||||
*/ |
||||
CONFLICTS_DURING_MERGE_BASE_CALCULATION |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Construct a NoMergeBase exception |
||||
* |
||||
* @param reason |
||||
* the reason why no merge base could be found |
||||
* @param message |
||||
* a text describing the problem |
||||
*/ |
||||
public NoMergeBaseException(MergeBaseFailureReason reason, String message) { |
||||
super(MessageFormat.format(JGitText.get().noMergeBase, |
||||
reason.toString(), message)); |
||||
this.reason = reason; |
||||
} |
||||
|
||||
/** |
||||
* Construct a NoMergeBase exception |
||||
* |
||||
* @param reason |
||||
* the reason why no merge base could be found |
||||
* @param message |
||||
* a text describing the problem |
||||
* @param why |
||||
* an exception causing this error |
||||
*/ |
||||
public NoMergeBaseException(MergeBaseFailureReason reason, String message, |
||||
Throwable why) { |
||||
super(MessageFormat.format(JGitText.get().noMergeBase, |
||||
reason.toString(), message)); |
||||
this.reason = reason; |
||||
initCause(why); |
||||
} |
||||
|
||||
/** |
||||
* @return the reason why no merge base could be found |
||||
*/ |
||||
public MergeBaseFailureReason getReason() { |
||||
return reason; |
||||
} |
||||
} |
@ -0,0 +1,274 @@
|
||||
/* |
||||
* Copyright (C) 2012, Research In Motion Limited |
||||
* Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com> |
||||
* and other copyright owners as documented in the project's IP log. |
||||
* |
||||
* This program and the accompanying materials are made available |
||||
* under the terms of the Eclipse Distribution License v1.0 which |
||||
* accompanies this distribution, is reproduced below, and is |
||||
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||
* |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or |
||||
* without modification, are permitted provided that the following |
||||
* conditions are met: |
||||
* |
||||
* - Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* - Redistributions in binary form must reproduce the above |
||||
* copyright notice, this list of conditions and the following |
||||
* disclaimer in the documentation and/or other materials provided |
||||
* with the distribution. |
||||
* |
||||
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||
* names of its contributors may be used to endorse or promote |
||||
* products derived from this software without specific prior |
||||
* written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
/* |
||||
* Contributors: |
||||
* George Young - initial API and implementation |
||||
* Christian Halstrick - initial API and implementation |
||||
*/ |
||||
package org.eclipse.jgit.merge; |
||||
|
||||
import java.io.IOException; |
||||
import java.text.MessageFormat; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.logging.Logger; |
||||
|
||||
import org.eclipse.jgit.dircache.DirCache; |
||||
import org.eclipse.jgit.dircache.DirCacheBuilder; |
||||
import org.eclipse.jgit.dircache.DirCacheEntry; |
||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
||||
import org.eclipse.jgit.errors.NoMergeBaseException; |
||||
import org.eclipse.jgit.internal.JGitText; |
||||
import org.eclipse.jgit.lib.CommitBuilder; |
||||
import org.eclipse.jgit.lib.ObjectId; |
||||
import org.eclipse.jgit.lib.ObjectInserter; |
||||
import org.eclipse.jgit.lib.PersonIdent; |
||||
import org.eclipse.jgit.lib.Repository; |
||||
import org.eclipse.jgit.revwalk.RevCommit; |
||||
import org.eclipse.jgit.revwalk.filter.RevFilter; |
||||
import org.eclipse.jgit.treewalk.TreeWalk; |
||||
import org.eclipse.jgit.treewalk.WorkingTreeIterator; |
||||
|
||||
/** |
||||
* A three-way merger performing a content-merge if necessary across multiple |
||||
* bases using recursion |
||||
* |
||||
* This merger extends the resolve merger and does several things differently: |
||||
* |
||||
* - allow more than one merge base, up to a maximum |
||||
* |
||||
* - uses "Lists" instead of Arrays for chained types |
||||
* |
||||
* - recursively merges the merge bases together to compute a usable base |
||||
* |
||||
*/ |
||||
|
||||
public class RecursiveMerger extends ResolveMerger { |
||||
static Logger log = Logger.getLogger(RecursiveMerger.class.toString()); |
||||
|
||||
/** |
||||
* The maximum number of merge bases. This merge will stop when the number |
||||
* of merge bases exceeds this value |
||||
*/ |
||||
public final int MAX_BASES = 200; |
||||
|
||||
private PersonIdent ident = new PersonIdent(db); |
||||
|
||||
/** |
||||
* Normal recursive merge when you want a choice of DirCache placement |
||||
* inCore |
||||
* |
||||
* @param local |
||||
* @param inCore |
||||
*/ |
||||
protected RecursiveMerger(Repository local, boolean inCore) { |
||||
super(local, inCore); |
||||
} |
||||
|
||||
/** |
||||
* Normal recursive merge, implies not inCore |
||||
* |
||||
* @param local |
||||
*/ |
||||
protected RecursiveMerger(Repository local) { |
||||
this(local, false); |
||||
} |
||||
|
||||
/** |
||||
* Get a single base commit for two given commits. If the two source commits |
||||
* have more than one base commit recursively merge the base commits |
||||
* together until you end up with a single base commit. |
||||
* |
||||
* @throws IOException |
||||
* @throws IncorrectObjectTypeException |
||||
*/ |
||||
@Override |
||||
protected RevCommit getBaseCommit(RevCommit a, RevCommit b) |
||||
throws IncorrectObjectTypeException, IOException { |
||||
return getBaseCommit(a, b, 0); |
||||
} |
||||
|
||||
/** |
||||
* Get a single base commit for two given commits. If the two source commits |
||||
* have more than one base commit recursively merge the base commits |
||||
* together until a virtual common base commit has been found. |
||||
* |
||||
* @param a |
||||
* the first commit to be merged |
||||
* @param b |
||||
* the second commit to be merged |
||||
* @param callDepth |
||||
* the callDepth when this method is called recursively |
||||
* @return the merge base of two commits |
||||
* @throws IOException |
||||
* @throws IncorrectObjectTypeException |
||||
* one of the input objects is not a commit. |
||||
* @throws NoMergeBaseException |
||||
* too many merge bases are found or the computation of a common |
||||
* merge base failed (e.g. because of a conflict). |
||||
*/ |
||||
protected RevCommit getBaseCommit(RevCommit a, RevCommit b, int callDepth) |
||||
throws IOException { |
||||
ArrayList<RevCommit> baseCommits = new ArrayList<RevCommit>(); |
||||
walk.reset(); |
||||
walk.setRevFilter(RevFilter.MERGE_BASE); |
||||
walk.markStart(a); |
||||
walk.markStart(b); |
||||
RevCommit c; |
||||
while ((c = walk.next()) != null) |
||||
baseCommits.add(c); |
||||
|
||||
if (baseCommits.isEmpty()) |
||||
return null; |
||||
if (baseCommits.size() == 1) |
||||
return baseCommits.get(0); |
||||
if (baseCommits.size() >= MAX_BASES) |
||||
throw new NoMergeBaseException(NoMergeBaseException.MergeBaseFailureReason.TOO_MANY_MERGE_BASES, MessageFormat.format( |
||||
JGitText.get().mergeRecursiveTooManyMergeBasesFor, |
||||
Integer.valueOf(MAX_BASES), a.name(), b.name(), |
||||
Integer.valueOf(baseCommits.size()))); |
||||
|
||||
// We know we have more than one base commit. We have to do merges now
|
||||
// to determine a single base commit. We don't want to spoil the current
|
||||
// dircache and working tree with the results of this intermediate
|
||||
// merges. Therefore set the dircache to a new in-memory dircache and
|
||||
// disable that we update the working-tree. We set this back to the
|
||||
// original values once a single base commit is created.
|
||||
RevCommit currentBase = baseCommits.get(0); |
||||
DirCache oldDircache = dircache; |
||||
boolean oldIncore = inCore; |
||||
WorkingTreeIterator oldWTreeIt = workingTreeIterator; |
||||
workingTreeIterator = null; |
||||
try { |
||||
dircache = dircacheFromTree(currentBase.getTree()); |
||||
inCore = true; |
||||
|
||||
List<RevCommit> parents = new ArrayList<RevCommit>(); |
||||
parents.add(currentBase); |
||||
for (int commitIdx = 1; commitIdx < baseCommits.size(); commitIdx++) { |
||||
RevCommit nextBase = baseCommits.get(commitIdx); |
||||
if (commitIdx >= MAX_BASES) |
||||
throw new NoMergeBaseException( |
||||
NoMergeBaseException.MergeBaseFailureReason.TOO_MANY_MERGE_BASES, |
||||
MessageFormat.format( |
||||
JGitText.get().mergeRecursiveTooManyMergeBasesFor, |
||||
Integer.valueOf(MAX_BASES), a.name(), b.name(), |
||||
Integer.valueOf(baseCommits.size()))); |
||||
parents.add(nextBase); |
||||
if (mergeTrees( |
||||
openTree(getBaseCommit(currentBase, nextBase, |
||||
callDepth + 1).getTree()), |
||||
currentBase.getTree(), |
||||
nextBase.getTree())) |
||||
currentBase = createCommitForTree(resultTree, parents); |
||||
else |
||||
throw new NoMergeBaseException( |
||||
NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION, |
||||
MessageFormat.format( |
||||
JGitText.get().mergeRecursiveTooManyMergeBasesFor, |
||||
Integer.valueOf(MAX_BASES), a.name(), |
||||
b.name(), |
||||
Integer.valueOf(baseCommits.size()))); |
||||
} |
||||
} finally { |
||||
inCore = oldIncore; |
||||
dircache = oldDircache; |
||||
workingTreeIterator = oldWTreeIt; |
||||
} |
||||
return currentBase; |
||||
} |
||||
|
||||
/** |
||||
* Create a new commit by explicitly specifying the content tree and the |
||||
* parents. The commit message is not set and author/committer are set to |
||||
* the current user. |
||||
* |
||||
* @param tree |
||||
* the tree this commit should capture |
||||
* @param parents |
||||
* the list of parent commits |
||||
* @return a new (persisted) commit |
||||
* @throws IOException |
||||
*/ |
||||
private RevCommit createCommitForTree(ObjectId tree, List<RevCommit> parents) |
||||
throws IOException { |
||||
CommitBuilder c = new CommitBuilder(); |
||||
c.setParentIds(parents); |
||||
c.setTreeId(tree); |
||||
c.setAuthor(ident); |
||||
c.setCommitter(ident); |
||||
ObjectInserter odi = db.newObjectInserter(); |
||||
ObjectId newCommitId = odi.insert(c); |
||||
odi.flush(); |
||||
RevCommit ret = walk.lookupCommit(newCommitId); |
||||
walk.parseHeaders(ret); |
||||
return ret; |
||||
} |
||||
|
||||
/** |
||||
* Create a new in memory dircache which has the same content as a given |
||||
* tree. |
||||
* |
||||
* @param treeId |
||||
* the tree which should be used to fill the dircache |
||||
* @return a new in memory dircache |
||||
* @throws IOException |
||||
*/ |
||||
private DirCache dircacheFromTree(ObjectId treeId) throws IOException { |
||||
DirCache ret = DirCache.newInCore(); |
||||
DirCacheBuilder builder = ret.builder(); |
||||
TreeWalk tw = new TreeWalk(db); |
||||
tw.addTree(treeId); |
||||
tw.setRecursive(true); |
||||
while (tw.next()) { |
||||
DirCacheEntry e = new DirCacheEntry(tw.getRawPath()); |
||||
e.setFileMode(tw.getFileMode(0)); |
||||
e.setObjectId(tw.getObjectId(0)); |
||||
builder.add(e); |
||||
} |
||||
builder.finish(); |
||||
return ret; |
||||
} |
||||
} |
@ -0,0 +1,67 @@
|
||||
/* |
||||
* Copyright (C) 2012, Research In Motion Limited |
||||
* and other copyright owners as documented in the project's IP log. |
||||
* |
||||
* This program and the accompanying materials are made available |
||||
* under the terms of the Eclipse Distribution License v1.0 which |
||||
* accompanies this distribution, is reproduced below, and is |
||||
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||
* |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or |
||||
* without modification, are permitted provided that the following |
||||
* conditions are met: |
||||
* |
||||
* - Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* - Redistributions in binary form must reproduce the above |
||||
* copyright notice, this list of conditions and the following |
||||
* disclaimer in the documentation and/or other materials provided |
||||
* with the distribution. |
||||
* |
||||
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||
* names of its contributors may be used to endorse or promote |
||||
* products derived from this software without specific prior |
||||
* written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package org.eclipse.jgit.merge; |
||||
|
||||
import org.eclipse.jgit.lib.Repository; |
||||
|
||||
/** |
||||
* A three-way merge strategy performing a content-merge if necessary |
||||
*/ |
||||
public class StrategyRecursive extends StrategyResolve { |
||||
|
||||
@Override |
||||
public ThreeWayMerger newMerger(Repository db) { |
||||
return new RecursiveMerger(db, false); |
||||
} |
||||
|
||||
@Override |
||||
public ThreeWayMerger newMerger(Repository db, boolean inCore) { |
||||
return new RecursiveMerger(db, inCore); |
||||
} |
||||
|
||||
@Override |
||||
public String getName() { |
||||
return "recursive"; |
||||
} |
||||
} |
Loading…
Reference in new issue