Browse Source

Added merge strategy RESOLVE

This adds the first merge strategy to JGit which does real
content-merges if necessary. The new merge strategy "resolve" takes as
input three commits: a common base, ours and theirs. It will simply takeover
changes on files which are only touched in ours or theirs. For files
touched in ours and theirs it will try to merge the two contents
knowing taking into account the specified common base.

Rename detection has not been introduced for now.

Change-Id: I49a5ebcdcf4f540f606092c0f1dc66c965dc66ba
Signed-off-by: Christian Halstrick <christian.halstrick@sap.com>
Signed-off-by: Stefan Lay <stefan.lay@sap.com>
stable-0.9
Christian Halstrick 15 years ago
parent
commit
45e79a526c
  1. 300
      org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
  2. 121
      org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
  3. 67
      org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java
  4. 12
      org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
  5. 4
      org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java
  6. 571
      org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
  7. 61
      org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java

300
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java

@ -45,20 +45,20 @@ package org.eclipse.jgit.api;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator;
import org.eclipse.jgit.errors.CheckoutConflictException; import org.eclipse.jgit.api.MergeResult.MergeStatus;
import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.GitIndex;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.lib.RepositoryTestCase; import org.eclipse.jgit.lib.RepositoryTestCase;
import org.eclipse.jgit.lib.WorkDirCheckout; import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.lib.GitIndex.Entry;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
public class MergeCommandTest extends RepositoryTestCase { public class MergeCommandTest extends RepositoryTestCase {
public void testMergeInItself() throws Exception { public void testMergeInItself() throws Exception {
Git git = new Git(db); Git git = new Git(db);
git.commit().setMessage("initial commit").call(); git.commit().setMessage("initial commit").call();
@ -97,13 +97,15 @@ public class MergeCommandTest extends RepositoryTestCase {
public void testFastForwardWithFiles() throws Exception { public void testFastForwardWithFiles() throws Exception {
Git git = new Git(db); Git git = new Git(db);
addNewFileToIndex("file1"); writeTrashFile("file1", "file1");
git.add().addFilepattern("file1").call();
RevCommit first = git.commit().setMessage("initial commit").call(); RevCommit first = git.commit().setMessage("initial commit").call();
assertTrue(new File(db.getWorkTree(), "file1").exists()); assertTrue(new File(db.getWorkTree(), "file1").exists());
createBranch(first, "refs/heads/branch1"); createBranch(first, "refs/heads/branch1");
addNewFileToIndex("file2"); writeTrashFile("file2", "file2");
git.add().addFilepattern("file2").call();
RevCommit second = git.commit().setMessage("second commit").call(); RevCommit second = git.commit().setMessage("second commit").call();
assertTrue(new File(db.getWorkTree(), "file2").exists()); assertTrue(new File(db.getWorkTree(), "file2").exists());
@ -121,14 +123,17 @@ public class MergeCommandTest extends RepositoryTestCase {
public void testMultipleHeads() throws Exception { public void testMultipleHeads() throws Exception {
Git git = new Git(db); Git git = new Git(db);
addNewFileToIndex("file1"); writeTrashFile("file1", "file1");
git.add().addFilepattern("file1").call();
RevCommit first = git.commit().setMessage("initial commit").call(); RevCommit first = git.commit().setMessage("initial commit").call();
createBranch(first, "refs/heads/branch1"); createBranch(first, "refs/heads/branch1");
addNewFileToIndex("file2"); writeTrashFile("file2", "file2");
git.add().addFilepattern("file2").call();
RevCommit second = git.commit().setMessage("second commit").call(); RevCommit second = git.commit().setMessage("second commit").call();
addNewFileToIndex("file3"); writeTrashFile("file3", "file3");
git.add().addFilepattern("file3").call();
git.commit().setMessage("third commit").call(); git.commit().setMessage("third commit").call();
checkoutBranch("refs/heads/branch1"); checkoutBranch("refs/heads/branch1");
@ -142,7 +147,245 @@ public class MergeCommandTest extends RepositoryTestCase {
merge.call(); merge.call();
fail("Expected exception not thrown when merging multiple heads"); fail("Expected exception not thrown when merging multiple heads");
} catch (InvalidMergeHeadsException e) { } catch (InvalidMergeHeadsException e) {
// expected this exception
}
}
public void testContentMerge() throws Exception {
Git git = new Git(db);
writeTrashFile("a", "1\na\n3\n");
writeTrashFile("b", "1\nb\n3\n");
writeTrashFile("c/c/c", "1\nc\n3\n");
git.add().addFilepattern("a").addFilepattern("b")
.addFilepattern("c/c/c").call();
RevCommit initialCommit = git.commit().setMessage("initial").call();
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
writeTrashFile("a", "1\na(side)\n3\n");
writeTrashFile("b", "1\nb(side)\n3\n");
git.add().addFilepattern("a").addFilepattern("b").call();
RevCommit secondCommit = git.commit().setMessage("side").call();
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
checkoutBranch("refs/heads/master");
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
writeTrashFile("a", "1\na(main)\n3\n");
writeTrashFile("c/c/c", "1\nc(main)\n3\n");
git.add().addFilepattern("a").addFilepattern("c/c/c").call();
git.commit().setMessage("main").call();
MergeResult result = git.merge().include(secondCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
assertEquals(
"1\n<<<<<<< HEAD\na(main)\n=======\na(side)\n>>>>>>> 86503e7e397465588cc267b65d778538bffccb83\n3\n",
read(new File(db.getWorkTree(), "a")));
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
assertEquals("1\nc(main)\n3\n",
read(new File(db.getWorkTree(), "c/c/c")));
assertEquals(1, result.getConflicts().size());
assertEquals(3, result.getConflicts().get("a")[0].length);
assertEquals(RepositoryState.MERGING, db.getRepositoryState());
}
public void testSuccessfulContentMerge() throws Exception {
Git git = new Git(db);
writeTrashFile("a", "1\na\n3\n");
writeTrashFile("b", "1\nb\n3\n");
writeTrashFile("c/c/c", "1\nc\n3\n");
git.add().addFilepattern("a").addFilepattern("b")
.addFilepattern("c/c/c").call();
RevCommit initialCommit = git.commit().setMessage("initial").call();
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
writeTrashFile("a", "1(side)\na\n3\n");
writeTrashFile("b", "1\nb(side)\n3\n");
git.add().addFilepattern("a").addFilepattern("b").call();
RevCommit secondCommit = git.commit().setMessage("side").call();
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
checkoutBranch("refs/heads/master");
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
writeTrashFile("a", "1\na\n3(main)\n");
writeTrashFile("c/c/c", "1\nc(main)\n3\n");
git.add().addFilepattern("a").addFilepattern("c/c/c").call();
RevCommit thirdCommit = git.commit().setMessage("main").call();
MergeResult result = git.merge().include(secondCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.MERGED, result.getMergeStatus());
assertEquals("1(side)\na\n3(main)\n", read(new File(db.getWorkTree(),
"a")));
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
assertEquals("1\nc(main)\n3\n", read(new File(db.getWorkTree(),
"c/c/c")));
assertEquals(null, result.getConflicts());
assertTrue(2 == result.getMergedCommits().length);
assertEquals(thirdCommit, result.getMergedCommits()[0]);
assertEquals(secondCommit, result.getMergedCommits()[1]);
Iterator<RevCommit> it = git.log().call().iterator();
RevCommit newHead = it.next();
assertEquals(newHead, result.getNewHead());
assertEquals(2, newHead.getParentCount());
assertEquals(thirdCommit, newHead.getParent(0));
assertEquals(secondCommit, newHead.getParent(1));
assertEquals(
"merging 3fa334456d236a92db020289fe0bf481d91777b4 into HEAD",
newHead.getFullMessage());
// @TODO fix me
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
// test index state
}
public void testSuccessfulContentMergeAndDirtyworkingTree()
throws Exception {
Git git = new Git(db);
writeTrashFile("a", "1\na\n3\n");
writeTrashFile("b", "1\nb\n3\n");
writeTrashFile("d", "1\nd\n3\n");
writeTrashFile("c/c/c", "1\nc\n3\n");
git.add().addFilepattern("a").addFilepattern("b")
.addFilepattern("c/c/c").addFilepattern("d").call();
RevCommit initialCommit = git.commit().setMessage("initial").call();
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
writeTrashFile("a", "1(side)\na\n3\n");
writeTrashFile("b", "1\nb(side)\n3\n");
git.add().addFilepattern("a").addFilepattern("b").call();
RevCommit secondCommit = git.commit().setMessage("side").call();
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
checkoutBranch("refs/heads/master");
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
writeTrashFile("a", "1\na\n3(main)\n");
writeTrashFile("c/c/c", "1\nc(main)\n3\n");
git.add().addFilepattern("a").addFilepattern("c/c/c").call();
RevCommit thirdCommit = git.commit().setMessage("main").call();
writeTrashFile("d", "--- dirty ---");
MergeResult result = git.merge().include(secondCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.MERGED, result.getMergeStatus());
assertEquals("1(side)\na\n3(main)\n", read(new File(db.getWorkTree(),
"a")));
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
assertEquals("1\nc(main)\n3\n", read(new File(db.getWorkTree(),
"c/c/c")));
assertEquals(null, result.getConflicts());
assertTrue(2 == result.getMergedCommits().length);
assertEquals(thirdCommit, result.getMergedCommits()[0]);
assertEquals(secondCommit, result.getMergedCommits()[1]);
Iterator<RevCommit> it = git.log().call().iterator();
RevCommit newHead = it.next();
assertEquals(newHead, result.getNewHead());
assertEquals(2, newHead.getParentCount());
assertEquals(thirdCommit, newHead.getParent(0));
assertEquals(secondCommit, newHead.getParent(1));
assertEquals(
"merging 064d54d98a4cdb0fed1802a21c656bfda67fe879 into HEAD",
newHead.getFullMessage());
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
} }
public void testMergeFailingWithDirtyWorkingTree() throws Exception {
Git git = new Git(db);
writeTrashFile("a", "1\na\n3\n");
writeTrashFile("b", "1\nb\n3\n");
git.add().addFilepattern("a").addFilepattern("b").call();
RevCommit initialCommit = git.commit().setMessage("initial").call();
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
writeTrashFile("a", "1(side)\na\n3\n");
writeTrashFile("b", "1\nb(side)\n3\n");
git.add().addFilepattern("a").addFilepattern("b").call();
RevCommit secondCommit = git.commit().setMessage("side").call();
assertEquals("1\nb(side)\n3\n", read(new File(db.getWorkTree(), "b")));
checkoutBranch("refs/heads/master");
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
writeTrashFile("a", "1\na\n3(main)\n");
git.add().addFilepattern("a").call();
git.commit().setMessage("main").call();
writeTrashFile("a", "--- dirty ---");
MergeResult result = git.merge().include(secondCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.FAILED, result.getMergeStatus());
assertEquals("--- dirty ---", read(new File(db.getWorkTree(), "a")));
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
assertEquals(null, result.getConflicts());
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
}
public void testMergeConflictFileFolder() throws Exception {
Git git = new Git(db);
writeTrashFile("a", "1\na\n3\n");
writeTrashFile("b", "1\nb\n3\n");
git.add().addFilepattern("a").addFilepattern("b").call();
RevCommit initialCommit = git.commit().setMessage("initial").call();
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
writeTrashFile("c/c/c", "1\nc(side)\n3\n");
writeTrashFile("d", "1\nd(side)\n3\n");
git.add().addFilepattern("c/c/c").addFilepattern("d").call();
RevCommit secondCommit = git.commit().setMessage("side").call();
checkoutBranch("refs/heads/master");
writeTrashFile("c", "1\nc(main)\n3\n");
writeTrashFile("d/d/d", "1\nd(main)\n3\n");
git.add().addFilepattern("c").addFilepattern("d/d/d").call();
git.commit().setMessage("main").call();
MergeResult result = git.merge().include(secondCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
assertEquals("1\na\n3\n", read(new File(db.getWorkTree(), "a")));
assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
assertEquals("1\nc(main)\n3\n", read(new File(db.getWorkTree(), "c")));
assertEquals("1\nd(main)\n3\n", read(new File(db.getWorkTree(), "d/d/d")));
assertEquals(null, result.getConflicts());
assertEquals(RepositoryState.MERGING, db.getRepositoryState());
} }
private void createBranch(ObjectId objectId, String branchName) throws IOException { private void createBranch(ObjectId objectId, String branchName) throws IOException {
@ -151,33 +394,18 @@ public class MergeCommandTest extends RepositoryTestCase {
updateRef.update(); updateRef.update();
} }
private void checkoutBranch(String branchName) throws Exception { private void checkoutBranch(String branchName) throws IllegalStateException, IOException {
File workDir = db.getWorkTree(); RevWalk walk = new RevWalk(db);
if (workDir != null) { RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD));
WorkDirCheckout workDirCheckout = new WorkDirCheckout(db, RevCommit branch = walk.parseCommit(db.resolve(branchName));
workDir, db.mapTree(Constants.HEAD), DirCacheCheckout dco = new DirCacheCheckout(db,
db.getIndex(), db.mapTree(branchName)); head.getTree().getId(), db.lockDirCache(),
workDirCheckout.setFailOnConflict(true); branch.getTree().getId());
try { dco.setFailOnConflict(true);
workDirCheckout.checkout(); dco.checkout();
} catch (CheckoutConflictException e) { walk.release();
throw new JGitInternalException(
"Couldn't check out because of conflicts", e);
}
}
// update the HEAD // update the HEAD
RefUpdate refUpdate = db.updateRef(Constants.HEAD); RefUpdate refUpdate = db.updateRef(Constants.HEAD);
refUpdate.link(branchName); refUpdate.link(branchName);
} }
private void addNewFileToIndex(String filename) throws IOException,
CorruptObjectException {
File writeTrashFile = writeTrashFile(filename, filename);
GitIndex index = db.getIndex();
Entry entry = index.add(db.getWorkTree(), writeTrashFile);
entry.update(writeTrashFile);
index.write();
}
} }

121
org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java

@ -43,28 +43,32 @@
*/ */
package org.eclipse.jgit.api; package org.eclipse.jgit.api;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Arrays;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import org.eclipse.jgit.JGitText; import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.api.MergeResult.MergeStatus; import org.eclipse.jgit.api.MergeResult.MergeStatus;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.GitIndex;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.WorkDirCheckout;
import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.Ref.Storage;
import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.FileTreeIterator;
/** /**
* A class used to execute a {@code Merge} command. It has setters for all * A class used to execute a {@code Merge} command. It has setters for all
@ -101,7 +105,7 @@ public class MergeCommand extends GitCommand<MergeResult> {
*/ */
public MergeResult call() throws NoHeadException, public MergeResult call() throws NoHeadException,
ConcurrentRefUpdateException, CheckoutConflictException, ConcurrentRefUpdateException, CheckoutConflictException,
InvalidMergeHeadsException { InvalidMergeHeadsException, WrongRepositoryStateException, NoMessageException {
checkCallable(); checkCallable();
if (commits.size() != 1) if (commits.size() != 1)
@ -109,8 +113,10 @@ public class MergeCommand extends GitCommand<MergeResult> {
commits.isEmpty() ? JGitText.get().noMergeHeadSpecified commits.isEmpty() ? JGitText.get().noMergeHeadSpecified
: MessageFormat.format( : MessageFormat.format(
JGitText.get().mergeStrategyDoesNotSupportHeads, JGitText.get().mergeStrategyDoesNotSupportHeads,
mergeStrategy.getName(), commits.size())); mergeStrategy.getName(),
Integer.valueOf(commits.size())));
RevWalk revWalk = null;
try { try {
Ref head = repo.getRef(Constants.HEAD); Ref head = repo.getRef(Constants.HEAD);
if (head == null) if (head == null)
@ -119,10 +125,10 @@ public class MergeCommand extends GitCommand<MergeResult> {
StringBuilder refLogMessage = new StringBuilder("merge "); StringBuilder refLogMessage = new StringBuilder("merge ");
// Check for FAST_FORWARD, ALREADY_UP_TO_DATE // Check for FAST_FORWARD, ALREADY_UP_TO_DATE
RevWalk revWalk = new RevWalk(repo); revWalk = new RevWalk(repo);
try {
RevCommit headCommit = revWalk.lookupCommit(head.getObjectId()); RevCommit headCommit = revWalk.lookupCommit(head.getObjectId());
// we know for know there is only one commit
Ref ref = commits.get(0); Ref ref = commits.get(0);
refLogMessage.append(ref.getName()); refLogMessage.append(ref.getName());
@ -135,58 +141,83 @@ public class MergeCommand extends GitCommand<MergeResult> {
RevCommit srcCommit = revWalk.lookupCommit(objectId); RevCommit srcCommit = revWalk.lookupCommit(objectId);
if (revWalk.isMergedInto(srcCommit, headCommit)) { if (revWalk.isMergedInto(srcCommit, headCommit)) {
setCallable(false); setCallable(false);
return new MergeResult(headCommit, srcCommit, return new MergeResult(headCommit, srcCommit, new ObjectId[] {
new ObjectId[] { srcCommit, headCommit }, headCommit, srcCommit },
MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy); MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy, null, null);
} else if (revWalk.isMergedInto(headCommit, srcCommit)) { } else if (revWalk.isMergedInto(headCommit, srcCommit)) {
// FAST_FORWARD detected: skip doing a real merge but only // FAST_FORWARD detected: skip doing a real merge but only
// update HEAD // update HEAD
refLogMessage.append(": " + MergeStatus.FAST_FORWARD); refLogMessage.append(": " + MergeStatus.FAST_FORWARD);
checkoutNewHead(revWalk, headCommit, srcCommit); DirCacheCheckout dco = new DirCacheCheckout(repo,
headCommit.getTree(), repo.lockDirCache(),
srcCommit.getTree());
dco.setFailOnConflict(true);
dco.checkout();
updateHead(refLogMessage, srcCommit, head.getObjectId()); updateHead(refLogMessage, srcCommit, head.getObjectId());
setCallable(false); setCallable(false);
return new MergeResult(srcCommit, headCommit, return new MergeResult(srcCommit, srcCommit, new ObjectId[] {
new ObjectId[] { srcCommit, headCommit }, headCommit, srcCommit }, MergeStatus.FAST_FORWARD,
MergeStatus.FAST_FORWARD, mergeStrategy); mergeStrategy, null, null);
} else {
repo.writeMergeCommitMsg("merging " + ref.getName() + " into "
+ head.getName());
repo.writeMergeHeads(Arrays.asList(ref.getObjectId()));
ThreeWayMerger merger = (ThreeWayMerger) mergeStrategy
.newMerger(repo);
boolean noProblems;
Map<String, org.eclipse.jgit.merge.MergeResult> lowLevelResults = null;
Map<String, MergeFailureReason> failingPaths = null;
if (merger instanceof ResolveMerger) {
ResolveMerger resolveMerger = (ResolveMerger) merger;
resolveMerger.setCommitNames(new String[] {
"BASE", "HEAD", ref.getName() });
resolveMerger.setWorkingTreeIt(new FileTreeIterator(repo));
noProblems = merger.merge(headCommit, srcCommit);
lowLevelResults = resolveMerger
.getMergeResults();
failingPaths = resolveMerger.getFailingPathes();
} else
noProblems = merger.merge(headCommit, srcCommit);
if (noProblems) {
DirCacheCheckout dco = new DirCacheCheckout(repo,
headCommit.getTree(), repo.lockDirCache(),
merger.getResultTreeId());
dco.setFailOnConflict(true);
dco.checkout();
RevCommit newHead = new Git(getRepository()).commit().call();
return new MergeResult(newHead.getId(),
null, new ObjectId[] {
headCommit.getId(), srcCommit.getId() },
MergeStatus.MERGED, mergeStrategy, null, null);
} else { } else {
return new MergeResult( if (failingPaths != null) {
headCommit, repo.writeMergeCommitMsg(null);
null, repo.writeMergeHeads(null);
new ObjectId[] { srcCommit, headCommit }, return new MergeResult(null,
MergeResult.MergeStatus.NOT_SUPPORTED, merger.getBaseCommit(0, 1),
mergeStrategy, new ObjectId[] {
JGitText.get().onlyAlreadyUpToDateAndFastForwardMergesAreAvailable); headCommit.getId(), srcCommit.getId() },
MergeStatus.FAILED, mergeStrategy,
lowLevelResults, null);
} else
return new MergeResult(null,
merger.getBaseCommit(0, 1),
new ObjectId[] { headCommit.getId(),
srcCommit.getId() },
MergeStatus.CONFLICTING, mergeStrategy,
lowLevelResults, null);
} }
} finally {
revWalk.release();
} }
} catch (IOException e) { } catch (IOException e) {
throw new JGitInternalException( throw new JGitInternalException(
MessageFormat.format( MessageFormat.format(
JGitText.get().exceptionCaughtDuringExecutionOfMergeCommand, JGitText.get().exceptionCaughtDuringExecutionOfMergeCommand,
e), e); e), e);
} } finally {
} if (revWalk != null)
revWalk.release();
private void checkoutNewHead(RevWalk revWalk, RevCommit headCommit,
RevCommit newHeadCommit) throws IOException,
CheckoutConflictException {
GitIndex index = repo.getIndex();
File workDir = repo.getWorkTree();
if (workDir != null) {
WorkDirCheckout workDirCheckout = new WorkDirCheckout(repo,
workDir, repo.mapTree(headCommit.getTree()), index,
repo.mapTree(newHeadCommit.getTree()));
workDirCheckout.setFailOnConflict(true);
try {
workDirCheckout.checkout();
} catch (org.eclipse.jgit.errors.CheckoutConflictException e) {
throw new CheckoutConflictException(
JGitText.get().couldNotCheckOutBecauseOfConflicts,
workDirCheckout.getConflicts(), e);
}
index.write();
} }
} }

67
org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java

@ -44,11 +44,15 @@
package org.eclipse.jgit.api; package org.eclipse.jgit.api;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.JGitText; import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.merge.MergeChunk;
import org.eclipse.jgit.merge.MergeChunk.ConflictState;
import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
/** /**
* Encapsulates the result of a {@link MergeCommand}. * Encapsulates the result of a {@link MergeCommand}.
@ -125,11 +129,14 @@ public class MergeResult {
* the status the merge resulted in * the status the merge resulted in
* @param mergeStrategy * @param mergeStrategy
* the used {@link MergeStrategy} * the used {@link MergeStrategy}
* @param lowLevelResults
* merge results as returned by {@link ResolveMerger#getMergeResults()}
*/ */
public MergeResult(ObjectId newHead, ObjectId base, public MergeResult(ObjectId newHead, ObjectId base,
ObjectId[] mergedCommits, MergeStatus mergeStatus, ObjectId[] mergedCommits, MergeStatus mergeStatus,
Map<String, org.eclipse.jgit.merge.MergeResult> lowLevelResults,
MergeStrategy mergeStrategy) { MergeStrategy mergeStrategy) {
this(newHead, base, mergedCommits, mergeStatus, mergeStrategy, null); this(newHead, base, mergedCommits, mergeStatus, mergeStrategy, lowLevelResults, null);
} }
/** /**
@ -145,18 +152,25 @@ public class MergeResult {
* the status the merge resulted in * the status the merge resulted in
* @param mergeStrategy * @param mergeStrategy
* the used {@link MergeStrategy} * the used {@link MergeStrategy}
* @param lowLevelResults
* merge results as returned by {@link ResolveMerger#getMergeResults()}
* @param description * @param description
* a user friendly description of the merge result * a user friendly description of the merge result
*/ */
public MergeResult(ObjectId newHead, ObjectId base, public MergeResult(ObjectId newHead, ObjectId base,
ObjectId[] mergedCommits, MergeStatus mergeStatus, ObjectId[] mergedCommits, MergeStatus mergeStatus,
MergeStrategy mergeStrategy, String description) { MergeStrategy mergeStrategy,
Map<String, org.eclipse.jgit.merge.MergeResult> lowLevelResults,
String description) {
this.newHead = newHead; this.newHead = newHead;
this.mergedCommits = mergedCommits; this.mergedCommits = mergedCommits;
this.base = base; this.base = base;
this.mergeStatus = mergeStatus; this.mergeStatus = mergeStatus;
this.mergeStrategy = mergeStrategy; this.mergeStrategy = mergeStrategy;
this.description = description; this.description = description;
if (lowLevelResults != null)
for (String path : lowLevelResults.keySet())
addConflict(path, lowLevelResults.get(path));
} }
/** /**
@ -214,6 +228,55 @@ public class MergeResult {
this.conflicts = conflicts; this.conflicts = conflicts;
} }
/**
* @param path
* @param conflictingRanges
* the conflicts to set
*/
public void addConflict(String path, int[][] conflictingRanges) {
if (conflicts == null)
conflicts = new HashMap<String, int[][]>();
conflicts.put(path, conflictingRanges);
}
/**
* @param path
* @param lowLevelResult
*/
public void addConflict(String path, org.eclipse.jgit.merge.MergeResult lowLevelResult) {
if (conflicts == null)
conflicts = new HashMap<String, int[][]>();
int nrOfConflicts = 0;
// just counting
for (MergeChunk mergeChunk : lowLevelResult) {
if (mergeChunk.getConflictState().equals(ConflictState.FIRST_CONFLICTING_RANGE)) {
nrOfConflicts++;
}
}
int currentConflict = -1;
int[][] ret=new int[nrOfConflicts][mergedCommits.length+1];
for (MergeChunk mergeChunk : lowLevelResult) {
// to store the end of this chunk (end of the last conflicting range)
int endOfChunk = 0;
if (mergeChunk.getConflictState().equals(ConflictState.FIRST_CONFLICTING_RANGE)) {
if (currentConflict > -1) {
// there was a previous conflicting range for which the end
// is not set yet - set it!
ret[currentConflict][mergedCommits.length] = endOfChunk;
}
currentConflict++;
endOfChunk = mergeChunk.getEnd();
ret[currentConflict][mergeChunk.getSequenceIndex()] = mergeChunk.getBegin();
}
if (mergeChunk.getConflictState().equals(ConflictState.NEXT_CONFLICTING_RANGE)) {
if (mergeChunk.getEnd() > endOfChunk)
endOfChunk = mergeChunk.getEnd();
ret[currentConflict][mergeChunk.getSequenceIndex()] = mergeChunk.getBegin();
}
}
conflicts.put(path, ret);
}
/** /**
* Returns information about the conflicts which occurred during a * Returns information about the conflicts which occurred during a
* {@link MergeCommand}. The returned value maps the path of a conflicting * {@link MergeCommand}. The returned value maps the path of a conflicting

12
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java vendored

@ -187,7 +187,8 @@ public class DirCacheCheckout {
public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc, public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc,
ObjectId mergeCommitTree) throws IOException { ObjectId mergeCommitTree) throws IOException {
this(repo, headCommitTree, dc, mergeCommitTree, new FileTreeIterator( this(repo, headCommitTree, dc, mergeCommitTree, new FileTreeIterator(
repo.getWorkTree(), repo.getFS(), WorkingTreeOptions.createDefaultInstance())); repo.getWorkTree(), repo.getFS(),
WorkingTreeOptions.createDefaultInstance()));
} }
/** /**
@ -341,7 +342,7 @@ public class DirCacheCheckout {
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
DirCacheEntry entry = dc.getEntry(path); DirCacheEntry entry = dc.getEntry(path);
checkoutEntry(file, entry, config_filemode()); checkoutEntry(repo, file, entry, config_filemode());
} }
@ -743,7 +744,8 @@ public class DirCacheCheckout {
NameConflictTreeWalk tw = new NameConflictTreeWalk(repo); NameConflictTreeWalk tw = new NameConflictTreeWalk(repo);
tw.reset(); tw.reset();
tw.addTree(new DirCacheIterator(dc)); tw.addTree(new DirCacheIterator(dc));
tw.addTree(new FileTreeIterator(repo.getWorkTree(), repo.getFS(), WorkingTreeOptions.createDefaultInstance())); tw.addTree(new FileTreeIterator(repo.getWorkTree(), repo.getFS(),
WorkingTreeOptions.createDefaultInstance()));
tw.setRecursive(true); tw.setRecursive(true);
tw.setFilter(PathFilter.create(path)); tw.setFilter(PathFilter.create(path));
DirCacheIterator dcIt; DirCacheIterator dcIt;
@ -769,7 +771,7 @@ public class DirCacheCheckout {
* TODO: this method works directly on File IO, we may need another * TODO: this method works directly on File IO, we may need another
* abstraction (like WorkingTreeIterator). This way we could tell e.g. * abstraction (like WorkingTreeIterator). This way we could tell e.g.
* Eclipse that Files in the workspace got changed * Eclipse that Files in the workspace got changed
* * @param repo
* @param f * @param f
* the file to be modified. The parent directory for this file * the file to be modified. The parent directory for this file
* has to exist already * has to exist already
@ -779,7 +781,7 @@ public class DirCacheCheckout {
* whether the mode bits should be handled at all. * whether the mode bits should be handled at all.
* @throws IOException * @throws IOException
*/ */
public void checkoutEntry(File f, DirCacheEntry entry, public static void checkoutEntry(final Repository repo, File f, DirCacheEntry entry,
boolean config_filemode) throws IOException { boolean config_filemode) throws IOException {
ObjectLoader ol = repo.open(entry.getObjectId()); ObjectLoader ol = repo.open(entry.getObjectId());
if (ol == null) if (ol == null)

4
org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java

@ -66,12 +66,16 @@ public abstract class MergeStrategy {
/** Simple strategy to merge paths, without simultaneous edits. */ /** Simple strategy to merge paths, without simultaneous edits. */
public static final ThreeWayMergeStrategy SIMPLE_TWO_WAY_IN_CORE = new StrategySimpleTwoWayInCore(); public static final ThreeWayMergeStrategy SIMPLE_TWO_WAY_IN_CORE = new StrategySimpleTwoWayInCore();
/** Simple strategy to merge paths. It tries to merge also contents. Multiple merge bases are not supported */
public static final ThreeWayMergeStrategy RESOLVE = new StrategyResolve();
private static final HashMap<String, MergeStrategy> STRATEGIES = new HashMap<String, MergeStrategy>(); private static final HashMap<String, MergeStrategy> STRATEGIES = new HashMap<String, MergeStrategy>();
static { static {
register(OURS); register(OURS);
register(THEIRS); register(THEIRS);
register(SIMPLE_TWO_WAY_IN_CORE); register(SIMPLE_TWO_WAY_IN_CORE);
register(RESOLVE);
} }
/** /**

571
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java

@ -0,0 +1,571 @@
/*
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>,
* Copyright (C) 2010, Matthias Sohn <matthias.sohn@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 java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuildIterator;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.IndexWriteException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
/**
* A three-way merger performing a content-merge if necessary
*/
public class ResolveMerger extends ThreeWayMerger {
/**
* If the merge fails abnormally (means: not because of unresolved
* conflicts) this enum is used to explain why it failed
*/
public enum MergeFailureReason {
/** the merge failed because of a dirty index */
DIRTY_INDEX,
/** the merge failed because of a dirty workingtree */
DIRTY_WORKTREE
}
private NameConflictTreeWalk tw;
private String commitNames[];
private static final int T_BASE = 0;
private static final int T_OURS = 1;
private static final int T_THEIRS = 2;
private static final int T_INDEX = 3;
private static final int T_FILE = 4;
private DirCacheBuilder builder;
private ObjectId resultTree;
private List<String> unmergedPathes = new ArrayList<String>();
private List<String> modifiedFiles = new LinkedList<String>();
private Map<String, DirCacheEntry> toBeCheckedOut = new HashMap<String, DirCacheEntry>();
private Map<String, MergeResult> mergeResults = new HashMap<String, MergeResult>();
private Map<String, MergeFailureReason> failingPathes = new HashMap<String, MergeFailureReason>();
private ObjectInserter oi;
private boolean enterSubtree;
private DirCache dircache;
private WorkingTreeIterator workingTreeIt;
/**
* @param local
*/
protected ResolveMerger(Repository local) {
super(local);
commitNames = new String[] { "BASE", "OURS", "THEIRS" };
oi = getObjectInserter();
}
@Override
protected boolean mergeImpl() throws IOException {
boolean implicitDirCache = false;
if (dircache == null) {
dircache = getRepository().lockDirCache();
implicitDirCache = true;
}
try {
builder = dircache.builder();
DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder);
tw = new NameConflictTreeWalk(db);
tw.reset();
tw.addTree(mergeBase());
tw.addTree(sourceTrees[0]);
tw.addTree(sourceTrees[1]);
tw.addTree(buildIt);
if (workingTreeIt != null)
tw.addTree(workingTreeIt);
while (tw.next()) {
if (!processEntry(
tw.getTree(T_BASE, CanonicalTreeParser.class),
tw.getTree(T_OURS, CanonicalTreeParser.class),
tw.getTree(T_THEIRS, CanonicalTreeParser.class),
tw.getTree(T_INDEX, DirCacheBuildIterator.class),
(workingTreeIt == null) ? null : tw.getTree(T_FILE, WorkingTreeIterator.class))) {
cleanUp();
return false;
}
if (tw.isSubtree() && enterSubtree)
tw.enterSubtree();
}
// All content-merges are successfully done. If we can now write the
// new
// index we are on quite safe ground. Even if the checkout of files
// coming from "theirs" fails the user can work around such failures
// by
// checking out the index again.
if (!builder.commit()) {
cleanUp();
throw new IndexWriteException();
}
builder = null;
// No problem found. The only thing left to be done is to checkout
// all files from "theirs" which have been selected to go into the
// new index.
checkout();
if (getUnmergedPathes().isEmpty()) {
resultTree = dircache.writeTree(oi);
return true;
} else {
resultTree = null;
return false;
}
} finally {
if (implicitDirCache)
dircache.unlock();
}
}
private void checkout() throws NoWorkTreeException, IOException {
for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut.entrySet()) {
File f = new File(db.getWorkTree(), entry.getKey());
createDir(f.getParentFile());
DirCacheCheckout.checkoutEntry(db,
f,
entry.getValue(), true);
modifiedFiles.add(entry.getKey());
}
}
private void createDir(File f) throws IOException {
if (!f.isDirectory() && !f.mkdirs()) {
File p = f;
while (p != null && !p.exists())
p = p.getParentFile();
if (p == null || p.isDirectory())
throw new IOException(JGitText.get().cannotCreateDirectory);
p.delete();
if (!f.mkdirs())
throw new IOException(JGitText.get().cannotCreateDirectory);
}
}
/**
* Reverts the worktree after an unsuccessful merge. We know that for all
* modified files the old content was in the old index and the index
* contained only stage 0
*
* @throws IOException
* @throws CorruptObjectException
* @throws NoWorkTreeException
*/
private void cleanUp() throws NoWorkTreeException, CorruptObjectException, IOException {
DirCache dc = db.readDirCache();
ObjectReader or = db.getObjectDatabase().newReader();
Iterator<String> mpathsIt=modifiedFiles.iterator();
while(mpathsIt.hasNext()) {
String mpath=mpathsIt.next();
DirCacheEntry entry = dc.getEntry(mpath);
FileOutputStream fos = new FileOutputStream(new File(db.getWorkTree(), mpath));
try {
or.open(entry.getObjectId()).copyTo(fos);
} finally {
fos.close();
}
mpathsIt.remove();
}
}
/**
* adds a new path with the specified stage to the index builder
*
* @param path
* @param p
* @param stage
* @return the entry which was added to the index
*/
private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage) {
if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) {
DirCacheEntry e = new DirCacheEntry(path, stage);
e.setFileMode(p.getEntryFileMode());
e.setObjectId(p.getEntryObjectId());
builder.add(e);
return e;
}
return null;
}
/**
* Processes one path and tries to merge. This method will do all do all
* trivial (not content) merges and will also detect if a merge will fail.
* The merge will fail when one of the following is true
* <ul>
* <li>the index entry does not match the entry in ours. When merging one
* branch into the current HEAD, ours will point to HEAD and theirs will
* point to the other branch. It is assumed that the index matches the HEAD
* because it will only not match HEAD if it was populated before the merge
* operation. But the merge commit should not accidentally contain
* modifications done before the merge. Check the <a href=
* "http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html#_3_way_merge"
* >git read-tree</a> documentation for further explanations.</li>
* <li>A conflict was detected and the working-tree file is dirty. When a
* conflict is detected the content-merge algorithm will try to write a
* merged version into the working-tree. If the file is dirty we would
* override unsaved data.</li>
*
* @param base
* the common base for ours and theirs
* @param ours
* the ours side of the merge. When merging a branch into the
* HEAD ours will point to HEAD
* @param theirs
* the theirs side of the merge. When merging a branch into the
* current HEAD theirs will point to the branch which is merged
* into HEAD.
* @param index
* the index entry
* @param work
* the file in the working tree
* @return <code>false</code> if the merge will fail because the index entry
* didn't match ours or the working-dir file was dirty and a
* conflict occured
* @throws MissingObjectException
* @throws IncorrectObjectTypeException
* @throws CorruptObjectException
* @throws IOException
*/
private boolean processEntry(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs,
DirCacheBuildIterator index, WorkingTreeIterator work)
throws MissingObjectException, IncorrectObjectTypeException,
CorruptObjectException, IOException {
enterSubtree = true;
final int modeO = tw.getRawMode(T_OURS);
final int modeI = tw.getRawMode(T_INDEX);
// Each index entry has to match ours, means: it has to be clean
if (nonTree(modeI)
&& !(tw.idEqual(T_INDEX, T_OURS) && modeO == modeI)) {
failingPathes.put(tw.getPathString(), MergeFailureReason.DIRTY_INDEX);
return false;
}
final int modeT = tw.getRawMode(T_THEIRS);
if (nonTree(modeO) && modeO == modeT && tw.idEqual(T_OURS, T_THEIRS)) {
// ours and theirs are equal: it doesn'nt matter
// which one we choose. OURS is choosen here.
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0);
// no checkout needed!
return true;
}
final int modeB = tw.getRawMode(T_BASE);
if (nonTree(modeO) && modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) {
// THEIRS was not changed compared to base. All changes must be in
// OURS. Choose OURS.
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0);
return true;
}
if (nonTree(modeT) && modeB == modeO && tw.idEqual(T_BASE, T_OURS)) {
// OURS was not changed compared to base. All changes must be in
// THEIRS. Choose THEIRS.
DirCacheEntry e=add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_0);
if (e!=null)
toBeCheckedOut.put(tw.getPathString(), e);
return true;
}
if (tw.isSubtree()) {
// file/folder conflicts: here I want to detect only file/folder
// conflict between ours and theirs. file/folder conflicts between
// base/index/workingTree and something else are not relevant or
// detected later
if (nonTree(modeO) && !nonTree(modeT)) {
if (nonTree(modeB))
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1);
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2);
unmergedPathes.add(tw.getPathString());
enterSubtree = false;
return true;
}
if (nonTree(modeT) && !nonTree(modeO)) {
if (nonTree(modeB))
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1);
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3);
unmergedPathes.add(tw.getPathString());
enterSubtree = false;
return true;
}
// ours and theirs are both folders or both files (and treewalk
// tells us we are in a subtree because of index or working-dir).
// If they are both folders no content-merge is required - we can
// return here.
if (!nonTree(modeO))
return true;
// ours and theirs are both files, just fall out of the if block
// and do the content merge
}
if (nonTree(modeO) && nonTree(modeT)) {
// We are going to update the worktree. Make sure the worktree is
// not modified
if (work != null
&& (!nonTree(work.getEntryRawMode()) || work.isModified(
index.getDirCacheEntry(), true, true, db.getFS()))) {
failingPathes.put(tw.getPathString(),
MergeFailureReason.DIRTY_WORKTREE);
return false;
}
if (!contentMerge(base, ours, theirs)) {
unmergedPathes.add(tw.getPathString());
}
modifiedFiles.add(tw.getPathString());
}
return true;
}
private boolean contentMerge(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs)
throws FileNotFoundException, IllegalStateException, IOException {
MergeFormatter fmt = new MergeFormatter();
// do the merge
MergeResult result = MergeAlgorithm.merge(
getRawText(base.getEntryObjectId(), db),
getRawText(ours.getEntryObjectId(), db),
getRawText(theirs.getEntryObjectId(), db));
File workTree = db.getWorkTree();
if (workTree == null)
// TODO: This should be handled by WorkingTreeIterators which
// support write operations
throw new UnsupportedOperationException();
File of = new File(workTree, tw.getPathString());
FileOutputStream fos = new FileOutputStream(of);
try {
fmt.formatMerge(fos, result, Arrays.asList(commitNames),
Constants.CHARACTER_ENCODING);
} finally {
fos.close();
}
if (result.containsConflicts()) {
// a conflict occured, the file will contain conflict markers
// the index will be populated with the three stages and only the
// workdir contains the halfways merged content
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1);
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2);
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3);
mergeResults.put(tw.getPathString(), result);
return false;
} else {
// no conflict occured, the file will contain fully merged content.
// the index will be populated with the new merged version
DirCacheEntry dce = new DirCacheEntry(tw.getPathString());
dce.setFileMode(tw.getFileMode(0));
dce.setLastModified(of.lastModified());
dce.setLength((int) of.length());
InputStream is = new FileInputStream(of);
try {
dce.setObjectId(oi.insert(Constants.OBJ_BLOB, of.length(),
is));
} finally {
is.close();
}
builder.add(dce);
return true;
}
}
private static RawText getRawText(ObjectId id, Repository db)
throws IOException {
if (id.equals(ObjectId.zeroId()))
return new RawText(new byte[] {});
return new RawText(db.open(id, Constants.OBJ_BLOB).getCachedBytes());
}
private static boolean nonTree(final int mode) {
return mode != 0 && !FileMode.TREE.equals(mode);
}
@Override
public ObjectId getResultTreeId() {
return (resultTree == null) ? null : resultTree.toObjectId();
}
/**
* @param commitNames
* the names of the commits as they would appear in conflict
* markers
*/
public void setCommitNames(String[] commitNames) {
this.commitNames = commitNames;
}
/**
* @return the names of the commits as they would appear in conflict
* markers.
*/
public String[] getCommitNames() {
return commitNames;
}
/**
* @return the paths with conflicts. This is a subset of the files listed
* by {@link #getModifiedFiles()}
*/
public List<String> getUnmergedPathes() {
return unmergedPathes;
}
/**
* @return the paths of files which have been modified by this merge. A
* file will be modified if a content-merge works on this path or if
* the merge algorithm decides to take the theirs-version. This is a
* superset of the files listed by {@link #getUnmergedPathes()}.
*/
public List<String> getModifiedFiles() {
return modifiedFiles;
}
/**
* @return a map which maps the paths of files which have to be checked out
* because the merge created new fully-merged content for this file
* into the index. This means: the merge wrote a new stage 0 entry
* for this path.
*/
public Map<String, DirCacheEntry> getToBeCheckedOut() {
return toBeCheckedOut;
}
/**
* @return the mergeResults
*/
public Map<String, MergeResult> getMergeResults() {
return mergeResults;
}
/**
* @return lists paths causing this merge to fail abnormally (not because of
* a conflict). <code>null</code> is returned if this merge didn't
* fail abnormally.
*/
public Map<String, MergeFailureReason> getFailingPathes() {
return (failingPathes.size() == 0) ? null : failingPathes;
}
/**
* Sets the DirCache which shall be used by this merger. If the DirCache is
* not set explicitly this merger will implicitly get and lock a default
* DirCache. If the DirCache is explicitly set the caller is responsible to
* lock it in advance. Finally the merger will call
* {@link DirCache#commit()} which requires that the DirCache is locked. If
* the {@link #mergeImpl()} returns without throwing an exception the lock
* will be released. In case of exceptions the caller is responsible to
* release the lock.
*
* @param dc
* the DirCache to set
*/
public void setDirCache(DirCache dc) {
this.dircache = dc;
}
/**
* Sets the WorkingTreeIterator to be used by this merger. If no
* WorkingTreeIterator is set this merger will ignore the working tree and
* fail if a content merge is necessary.
* <p>
* TODO: enhance WorkingTreeIterator to support write operations. Then this
* merger will be able to merge with a different working tree abstraction.
*
* @param workingTreeIt
* the workingTreeIt to set
*/
public void setWorkingTreeIt(WorkingTreeIterator workingTreeIt) {
this.workingTreeIt = workingTreeIt;
}
}

61
org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java

@ -0,0 +1,61 @@
/*
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>,
* Copyright (C) 2010, Matthias Sohn <matthias.sohn@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 org.eclipse.jgit.lib.Repository;
/**
* A three-way merge strategy performing a content-merge if necessary
*/
public class StrategyResolve extends ThreeWayMergeStrategy {
@Override
public ThreeWayMerger newMerger(Repository db) {
return new ResolveMerger(db);
}
@Override
public String getName() {
return "resolve";
}
}
Loading…
Cancel
Save