diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF index b20464e39..327a697f6 100644 --- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF @@ -14,6 +14,7 @@ Import-Package: org.eclipse.jgit.api;version="[4.0.0,4.1.0)", org.eclipse.jgit.internal.storage.file;version="[4.0.0,4.1.0)", org.eclipse.jgit.internal.storage.pack;version="[4.0.0,4.1.0)", org.eclipse.jgit.lib;version="[4.0.0,4.1.0)", + org.eclipse.jgit.merge;version="[4.0.0,4.1.0)", org.eclipse.jgit.revwalk;version="[4.0.0,4.1.0)", org.eclipse.jgit.storage.file;version="[4.0.0,4.1.0)", org.eclipse.jgit.treewalk;version="[4.0.0,4.1.0)", diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java index ce33ec709..925a6b021 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java @@ -52,11 +52,13 @@ import java.io.OutputStream; import java.security.MessageDigest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TimeZone; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.dircache.DirCache; @@ -88,6 +90,8 @@ import org.eclipse.jgit.lib.RefWriter; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TagBuilder; +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.ObjectWalk; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; @@ -187,6 +191,11 @@ public Date getClock() { return new Date(now); } + /** @return timezone used for default identities. */ + public TimeZone getTimeZone() { + return defaultCommitter.getTimeZone(); + } + /** * Adjust the current time that will used by the next commit. * @@ -615,6 +624,59 @@ public void reset(String name) throws Exception { } } + /** + * Cherry-pick a commit onto HEAD. + *

+ * This differs from {@code git cherry-pick} in that it works in a bare + * repository. As a result, any merge failure results in an exception, as + * there is no way to recover. + * + * @param id + * commit-ish to cherry-pick. + * @return newly created commit, or null if no work was done due to the + * resulting tree being identical. + * @throws Exception + */ + public RevCommit cherryPick(AnyObjectId id) throws Exception { + RevCommit commit = pool.parseCommit(id); + pool.parseBody(commit); + if (commit.getParentCount() != 1) + throw new IOException(String.format( + "Expected 1 parent for %s, found: %s", + id.name(), Arrays.asList(commit.getParents()))); + RevCommit parent = commit.getParent(0); + pool.parseHeaders(parent); + + Ref headRef = db.getRef(Constants.HEAD); + if (headRef == null) + throw new IOException("Missing HEAD"); + RevCommit head = pool.parseCommit(headRef.getObjectId()); + + ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true); + merger.setBase(parent.getTree()); + if (merger.merge(head, commit)) { + if (AnyObjectId.equals(head.getTree(), merger.getResultTreeId())) + return null; + tick(1); + org.eclipse.jgit.lib.CommitBuilder b = + new org.eclipse.jgit.lib.CommitBuilder(); + b.setParentId(head); + b.setTreeId(merger.getResultTreeId()); + b.setAuthor(commit.getAuthorIdent()); + b.setCommitter(new PersonIdent(defaultCommitter, new Date(now))); + b.setMessage(commit.getFullMessage()); + ObjectId result; + try (ObjectInserter ins = inserter) { + result = ins.insert(b); + ins.flush(); + } + update(Constants.HEAD, result); + return pool.parseCommit(result); + } else { + throw new IOException("Merge conflict"); + } + } + /** * Update the dumb client server info files. * diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java index cefc779a2..fbb9eecdf 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java @@ -46,6 +46,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -55,9 +56,9 @@ import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; -import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; @@ -296,6 +297,83 @@ public void commitToUnbornHead() throws Exception { assertEquals("refs/heads/master", ref.getTarget().getName()); } + @Test + public void cherryPick() throws Exception { + repo.updateRef("HEAD").link("refs/heads/master"); + RevCommit head = tr.branch("master").commit() + .add("foo", "foo contents\n") + .create(); + rw.parseBody(head); + RevCommit toPick = tr.commit() + .parent(tr.commit().create()) // Can't cherry-pick root. + .author(new PersonIdent("Cherrypick Author", "cpa@example.com", + tr.getClock(), tr.getTimeZone())) + .author(new PersonIdent("Cherrypick Committer", "cpc@example.com", + tr.getClock(), tr.getTimeZone())) + .message("message to cherry-pick") + .add("bar", "bar contents\n") + .create(); + RevCommit result = tr.cherryPick(toPick); + rw.parseBody(result); + + Ref headRef = tr.getRepository().getRef("HEAD"); + assertEquals(result, headRef.getObjectId()); + assertTrue(headRef.isSymbolic()); + assertEquals("refs/heads/master", headRef.getLeaf().getName()); + + assertEquals(1, result.getParentCount()); + assertEquals(head, result.getParent(0)); + assertEquals(toPick.getAuthorIdent(), result.getAuthorIdent()); + + // Committer name/email matches default, and time was incremented. + assertEquals(new PersonIdent(head.getCommitterIdent(), new Date(0)), + new PersonIdent(result.getCommitterIdent(), new Date(0))); + assertTrue(toPick.getCommitTime() < result.getCommitTime()); + + assertEquals("message to cherry-pick", result.getFullMessage()); + assertEquals("foo contents\n", blobAsString(result, "foo")); + assertEquals("bar contents\n", blobAsString(result, "bar")); + } + + @Test + public void cherryPickWithContentMerge() throws Exception { + RevCommit base = tr.branch("HEAD").commit() + .add("foo", "foo contents\n\n") + .create(); + tr.branch("HEAD").commit() + .add("foo", "foo contents\n\nlast line\n") + .create(); + RevCommit toPick = tr.commit() + .message("message to cherry-pick") + .parent(base) + .add("foo", "changed foo contents\n\n") + .create(); + RevCommit result = tr.cherryPick(toPick); + rw.parseBody(result); + + assertEquals("message to cherry-pick", result.getFullMessage()); + assertEquals("changed foo contents\n\nlast line\n", + blobAsString(result, "foo")); + } + + @Test + public void cherryPickWithIdenticalContents() throws Exception { + RevCommit base = tr.branch("HEAD").commit().add("foo", "foo contents\n") + .create(); + RevCommit head = tr.branch("HEAD").commit() + .parent(base) + .add("bar", "bar contents\n") + .create(); + RevCommit toPick = tr.commit() + .parent(base) + .message("message to cherry-pick") + .add("bar", "bar contents\n") + .create(); + assertNotEquals(head, toPick); + assertNull(tr.cherryPick(toPick)); + assertEquals(head, repo.getRef("HEAD").getObjectId()); + } + private String blobAsString(AnyObjectId treeish, String path) throws Exception { RevObject obj = tr.get(rw.parseTree(treeish), path);