diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java index 3aec611f4..9b597d32d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, GitHub Inc. + * Copyright (C) 2011-2012, GitHub Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -44,6 +44,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.File; @@ -68,7 +69,7 @@ import org.junit.Test; /** - * Unit tests of {@link CommitCommand} + * Unit tests of {@link CommitCommand}. */ public class CommitCommandTest extends RepositoryTestCase { @@ -365,4 +366,42 @@ public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception { assertEquals(file1Size, cache.getEntry("file1.txt").getLength()); assertEquals(0, cache.getEntry("file2.txt").getLength()); } + + @Test + public void commitAfterSquashMerge() throws Exception { + Git git = new Git(db); + + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit first = git.commit().setMessage("initial commit").call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + createBranch(first, "refs/heads/branch1"); + checkoutBranch("refs/heads/branch1"); + + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); + git.commit().setMessage("second commit").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + checkoutBranch("refs/heads/master"); + + MergeResult result = git.merge().include(db.getRef("branch1")) + .setSquash(true).call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + assertEquals(MergeResult.MergeStatus.FAST_FORWARD_SQUASHED, + result.getMergeStatus()); + + // comment not set, should be inferred from SQUASH_MSG + RevCommit squashedCommit = git.commit().call(); + + assertEquals(1, squashedCommit.getParentCount()); + assertNull(db.readSquashCommitMsg()); + assertEquals("commit: Squashed commit of the following:", db + .getReflogReader(Constants.HEAD).getLastEntry().getComment()); + assertEquals("commit: Squashed commit of the following:", db + .getReflogReader(db.getBranch()).getLastEntry().getComment()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java index 30a94520d..c6875b483 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Stefan Lay - * Copyright (C) 2010, Christian Halstrick + * Copyright (C) 2010-2012, Christian Halstrick * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -45,6 +45,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -62,6 +63,9 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.GitDateFormatter; +import org.eclipse.jgit.util.GitDateFormatter.Format; +import org.junit.Before; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; @@ -74,6 +78,15 @@ public class MergeCommandTest extends RepositoryTestCase { public static @DataPoints MergeStrategy[] mergeStrategies = MergeStrategy.get(); + private GitDateFormatter dateFormatter; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + dateFormatter = new GitDateFormatter(Format.DEFAULT); + } + @Test public void testMergeInItself() throws Exception { Git git = new Git(db); @@ -1096,6 +1109,180 @@ public void testFileModeMergeWithDirtyWorkTree() throws Exception { assertFalse(canExecute(git, "mergeableButDirty")); } + @Test + public void testSquashFastForward() throws Exception { + Git git = new Git(db); + + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit first = git.commit().setMessage("initial commit").call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + createBranch(first, "refs/heads/branch1"); + checkoutBranch("refs/heads/branch1"); + + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); + RevCommit second = git.commit().setMessage("second commit").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + writeTrashFile("file3", "file3"); + git.add().addFilepattern("file3").call(); + RevCommit third = git.commit().setMessage("third commit").call(); + assertTrue(new File(db.getWorkTree(), "file3").exists()); + + checkoutBranch("refs/heads/master"); + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertFalse(new File(db.getWorkTree(), "file2").exists()); + assertFalse(new File(db.getWorkTree(), "file3").exists()); + + MergeResult result = git.merge().include(db.getRef("branch1")) + .setSquash(true).call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + assertTrue(new File(db.getWorkTree(), "file3").exists()); + assertEquals(MergeResult.MergeStatus.FAST_FORWARD_SQUASHED, + result.getMergeStatus()); + assertEquals(first, result.getNewHead()); // HEAD didn't move + assertEquals(first, db.resolve(Constants.HEAD + "^{commit}")); + + assertEquals( + "Squashed commit of the following:\n\ncommit " + + third.getName() + + "\nAuthor: " + + third.getAuthorIdent().getName() + + " <" + + third.getAuthorIdent().getEmailAddress() + + ">\nDate: " + + dateFormatter.formatDate(third + .getAuthorIdent()) + + "\n\n\tthird commit\n\ncommit " + + second.getName() + + "\nAuthor: " + + second.getAuthorIdent().getName() + + " <" + + second.getAuthorIdent().getEmailAddress() + + ">\nDate: " + + dateFormatter.formatDate(second + .getAuthorIdent()) + "\n\n\tsecond commit\n", + db.readSquashCommitMsg()); + assertNull(db.readMergeCommitMsg()); + + Status stat = git.status().call(); + assertEquals(StatusCommandTest.set("file2", "file3"), stat.getAdded()); + } + + @Test + public void testSquashMerge() throws Exception { + Git git = new Git(db); + + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit first = git.commit().setMessage("initial commit").call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + createBranch(first, "refs/heads/branch1"); + + writeTrashFile("file2", "file2"); + git.add().addFilepattern("file2").call(); + RevCommit second = git.commit().setMessage("second commit").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + checkoutBranch("refs/heads/branch1"); + + writeTrashFile("file3", "file3"); + git.add().addFilepattern("file3").call(); + RevCommit third = git.commit().setMessage("third commit").call(); + assertTrue(new File(db.getWorkTree(), "file3").exists()); + + checkoutBranch("refs/heads/master"); + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + assertFalse(new File(db.getWorkTree(), "file3").exists()); + + MergeResult result = git.merge().include(db.getRef("branch1")) + .setSquash(true).call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + assertTrue(new File(db.getWorkTree(), "file3").exists()); + assertEquals(MergeResult.MergeStatus.MERGED_SQUASHED, + result.getMergeStatus()); + assertEquals(second, result.getNewHead()); // HEAD didn't move + assertEquals(second, db.resolve(Constants.HEAD + "^{commit}")); + + assertEquals( + "Squashed commit of the following:\n\ncommit " + + third.getName() + + "\nAuthor: " + + third.getAuthorIdent().getName() + + " <" + + third.getAuthorIdent().getEmailAddress() + + ">\nDate: " + + dateFormatter.formatDate(third + .getAuthorIdent()) + "\n\n\tthird commit\n", + db.readSquashCommitMsg()); + assertNull(db.readMergeCommitMsg()); + + Status stat = git.status().call(); + assertEquals(StatusCommandTest.set("file3"), stat.getAdded()); + } + + @Test + public void testSquashMergeConflict() throws Exception { + Git git = new Git(db); + + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit first = git.commit().setMessage("initial commit").call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + createBranch(first, "refs/heads/branch1"); + + writeTrashFile("file2", "master"); + git.add().addFilepattern("file2").call(); + RevCommit second = git.commit().setMessage("second commit").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + checkoutBranch("refs/heads/branch1"); + + writeTrashFile("file2", "branch"); + git.add().addFilepattern("file2").call(); + RevCommit third = git.commit().setMessage("third commit").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + checkoutBranch("refs/heads/master"); + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + MergeResult result = git.merge().include(db.getRef("branch1")) + .setSquash(true).call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + assertEquals(MergeResult.MergeStatus.CONFLICTING, + result.getMergeStatus()); + assertNull(result.getNewHead()); + assertEquals(second, db.resolve(Constants.HEAD + "^{commit}")); + + assertEquals( + "Squashed commit of the following:\n\ncommit " + + third.getName() + + "\nAuthor: " + + third.getAuthorIdent().getName() + + " <" + + third.getAuthorIdent().getEmailAddress() + + ">\nDate: " + + dateFormatter.formatDate(third + .getAuthorIdent()) + "\n\n\tthird commit\n", + db.readSquashCommitMsg()); + assertEquals("\nConflicts:\n\tfile2\n", db.readMergeCommitMsg()); + + Status stat = git.status().call(); + assertEquals(StatusCommandTest.set("file2"), stat.getConflicting()); + } + private void setExecutable(Git git, String path, boolean executable) { FS.DETECTED.setExecute( new File(git.getRepository().getWorkTree(), path), executable); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java index 0806cf0dd..c55775c9f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Chris Aniszczyk + * Copyright (C) 2011-2012, Chris Aniszczyk * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -46,6 +46,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -359,6 +360,37 @@ public void testHardResetOnTag() throws Exception { assertTrue(head.equals(secondCommit)); } + @Test + public void testHardResetAfterSquashMerge() throws Exception { + Git g = new Git(db); + + writeTrashFile("file1", "file1"); + g.add().addFilepattern("file1").call(); + RevCommit first = g.commit().setMessage("initial commit").call(); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + createBranch(first, "refs/heads/branch1"); + checkoutBranch("refs/heads/branch1"); + + writeTrashFile("file2", "file2"); + g.add().addFilepattern("file2").call(); + g.commit().setMessage("second commit").call(); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + + checkoutBranch("refs/heads/master"); + + MergeResult result = g.merge().include(db.getRef("branch1")) + .setSquash(true).call(); + + assertEquals(MergeResult.MergeStatus.FAST_FORWARD_SQUASHED, + result.getMergeStatus()); + assertNotNull(db.readSquashCommitMsg()); + + g.reset().setMode(ResetType.HARD).setRef(first.getName()).call(); + + assertNull(db.readSquashCommitMsg()); + } + private void assertReflog(ObjectId prevHead, ObjectId head) throws IOException { // Check the reflog for HEAD diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/MergeHeadMsgTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/MergeHeadMsgTest.java index 4e128e7f2..ed1f96776 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/MergeHeadMsgTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/MergeHeadMsgTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Christian Halstrick + * Copyright (C) 2010-2012 Christian Halstrick * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -78,7 +78,7 @@ public void testReadWriteMergeHeads() throws IOException { assertEquals(db.readMergeHeads().size(), 2); assertEquals(db.readMergeHeads().get(0), ObjectId.zeroId()); assertEquals(db.readMergeHeads().get(1), ObjectId.fromString(sampleId)); - db.writeMergeHeads(Collections.EMPTY_LIST); + db.writeMergeHeads(Collections. emptyList()); assertEquals(read(new File(db.getDirectory(), "MERGE_HEAD")), ""); assertEquals(db.readMergeHeads(), null); fos = new FileOutputStream(new File(db.getDirectory(), diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SquashCommitMsgTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SquashCommitMsgTest.java new file mode 100644 index 000000000..4374e4747 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SquashCommitMsgTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2012, IBM Corporation and others. + * 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.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.junit.Test; + +public class SquashCommitMsgTest extends RepositoryTestCase { + private static final String squashMsg = "squashed commit"; + + @Test + public void testReadWriteMergeMsg() throws IOException { + assertEquals(db.readSquashCommitMsg(), null); + assertFalse(new File(db.getDirectory(), Constants.SQUASH_MSG).exists()); + db.writeSquashCommitMsg(squashMsg); + assertEquals(squashMsg, db.readSquashCommitMsg()); + assertEquals(read(new File(db.getDirectory(), Constants.SQUASH_MSG)), + squashMsg); + db.writeSquashCommitMsg(null); + assertEquals(db.readSquashCommitMsg(), null); + assertFalse(new File(db.getDirectory(), Constants.SQUASH_MSG).exists()); + FileOutputStream fos = new FileOutputStream(new File(db.getDirectory(), + Constants.SQUASH_MSG)); + try { + fos.write(squashMsg.getBytes(Constants.CHARACTER_ENCODING)); + } finally { + fos.close(); + } + assertEquals(db.readSquashCommitMsg(), squashMsg); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SquashMessageFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SquashMessageFormatterTest.java new file mode 100644 index 000000000..4274d8baf --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SquashMessageFormatterTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2012, IBM Corporation and others. + * 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 java.util.Arrays; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.SampleDataRepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.GitDateFormatter; +import org.eclipse.jgit.util.GitDateFormatter.Format; +import org.junit.Before; +import org.junit.Test; + +/** + * Test construction of squash message by {@link SquashMessageFormatterTest}. + */ +public class SquashMessageFormatterTest extends SampleDataRepositoryTestCase { + private GitDateFormatter dateFormatter; + private SquashMessageFormatter msgFormatter; + private RevCommit revCommit; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + dateFormatter = new GitDateFormatter(Format.DEFAULT); + msgFormatter = new SquashMessageFormatter(); + } + + @Test + public void testCommit() throws Exception { + Git git = new Git(db); + revCommit = git.commit().setMessage("squash_me").call(); + + Ref master = db.getRef("refs/heads/master"); + String message = msgFormatter.format(Arrays.asList(revCommit), master); + assertEquals( + "Squashed commit of the following:\n\ncommit " + + revCommit.getName() + "\nAuthor: " + + revCommit.getAuthorIdent().getName() + " <" + + revCommit.getAuthorIdent().getEmailAddress() + + ">\nDate: " + dateFormatter.formatDate(author) + + "\n\n\tsquash_me\n", message); + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index be6080228..1131c1560 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -424,6 +424,7 @@ sourceDestinationMustMatch=Source/Destination must match. sourceIsNotAWildcard=Source is not a wildcard. sourceRefDoesntResolveToAnyObject=Source ref {0} doesn't resolve to any object. sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0} +squashCommitNotUpdatingHEAD=Squash commit -- not updating HEAD staleRevFlagsOn=Stale RevFlags on {0} startingReadStageWithoutWrittenRequestDataPendingIsNotSupported=Starting read stage without written request data pending is not supported stashApplyFailed=Applying stashed changes did not successfully complete diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index eac6fe6a4..ae6d62963 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Christian Halstrick + * Copyright (C) 2010-2012, Christian Halstrick * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -488,9 +488,20 @@ private void processOptions(RepositoryState state) throws NoMessageException { Constants.MERGE_MSG, e), e); } } + } else if (state == RepositoryState.SAFE && message == null) { + try { + message = repo.readSquashCommitMsg(); + if (message != null) + repo.writeSquashCommitMsg(null /* delete */); + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, + Constants.MERGE_MSG, e), e); + } + } if (message == null) - // as long as we don't suppport -C option we have to have + // as long as we don't support -C option we have to have // an explicit message throw new NoMessageException(JGitText.get().commitMessageNotSpecified); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java index c5a955211..3ca861c06 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Christian Halstrick - * Copyright (C) 2010, Stefan Lay + * Copyright (C) 2010-2012, Stefan Lay * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -76,8 +76,10 @@ import org.eclipse.jgit.merge.Merger; import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; +import org.eclipse.jgit.merge.SquashMessageFormatter; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.RevWalkUtils; import org.eclipse.jgit.treewalk.FileTreeIterator; /** @@ -95,6 +97,8 @@ public class MergeCommand extends GitCommand { private List commits = new LinkedList(); + private boolean squash; + /** * @param repo */ @@ -184,18 +188,41 @@ public MergeResult call() throws GitAPIException, NoHeadException, srcCommit.getTree()); dco.setFailOnConflict(true); dco.checkout(); - - updateHead(refLogMessage, srcCommit, headId); + String msg = null; + ObjectId newHead, base = null; + MergeStatus mergeStatus = null; + if (!squash) { + updateHead(refLogMessage, srcCommit, headId); + newHead = base = srcCommit; + mergeStatus = MergeStatus.FAST_FORWARD; + } else { + msg = JGitText.get().squashCommitNotUpdatingHEAD; + newHead = base = headId; + mergeStatus = MergeStatus.FAST_FORWARD_SQUASHED; + List squashedCommits = RevWalkUtils.find( + revWalk, srcCommit, headCommit); + String squashMessage = new SquashMessageFormatter().format( + squashedCommits, head); + repo.writeSquashCommitMsg(squashMessage); + } setCallable(false); - return new MergeResult(srcCommit, srcCommit, new ObjectId[] { - headCommit, srcCommit }, MergeStatus.FAST_FORWARD, - mergeStrategy, null, null); + return new MergeResult(newHead, base, new ObjectId[] { + headCommit, srcCommit }, mergeStatus, mergeStrategy, + null, msg); } else { - - String mergeMessage = new MergeMessageFormatter().format( - commits, head); - repo.writeMergeCommitMsg(mergeMessage); - repo.writeMergeHeads(Arrays.asList(ref.getObjectId())); + String mergeMessage = ""; + if (!squash) { + mergeMessage = new MergeMessageFormatter().format( + commits, head); + repo.writeMergeCommitMsg(mergeMessage); + repo.writeMergeHeads(Arrays.asList(ref.getObjectId())); + } else { + List squashedCommits = RevWalkUtils.find( + revWalk, srcCommit, headCommit); + String squashMessage = new SquashMessageFormatter().format( + squashedCommits, head); + repo.writeSquashCommitMsg(squashMessage); + } Merger merger = mergeStrategy.newMerger(repo); boolean noProblems; Map> lowLevelResults = null; @@ -223,12 +250,22 @@ public MergeResult call() throws GitAPIException, NoHeadException, dco.setFailOnConflict(true); dco.checkout(); - RevCommit newHead = new Git(getRepository()).commit() + String msg = null; + RevCommit newHead = null; + MergeStatus mergeStatus = null; + if (!squash) { + newHead = new Git(getRepository()).commit() .setReflogComment(refLogMessage.toString()).call(); - return new MergeResult(newHead.getId(), - null, new ObjectId[] { - headCommit.getId(), srcCommit.getId() }, - MergeStatus.MERGED, mergeStrategy, null, null); + mergeStatus = MergeStatus.MERGED; + } else { + msg = JGitText.get().squashCommitNotUpdatingHEAD; + newHead = headCommit; + mergeStatus = MergeStatus.MERGED_SQUASHED; + } + return new MergeResult(newHead.getId(), null, + new ObjectId[] { headCommit.getId(), + srcCommit.getId() }, mergeStatus, + mergeStrategy, null, msg); } else { if (failingPaths != null) { repo.writeMergeCommitMsg(null); @@ -334,4 +371,25 @@ public MergeCommand include(String name, AnyObjectId commit) { return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name, commit.copy())); } + + /** + * If true, will prepare the next commit in working tree and + * index as if a real merge happened, but do not make the commit or move the + * HEAD. Otherwise, perform the merge and commit the result. + *

+ * In case the merge was successful but this flag was set to + * true a {@link MergeResult} with status + * {@link MergeStatus#MERGED_SQUASHED} or + * {@link MergeStatus#FAST_FORWARD_SQUASHED} is returned. + * + * @param squash + * whether to squash commits or not + * @return {@code this} + * @since 2.0 + */ + public MergeCommand setSquash(boolean squash) { + checkCallable(); + this.squash = squash; + return this; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java index c1733f5b7..484039e70 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Stefan Lay - * Copyright (C) 2010, Christian Halstrick + * Copyright (C) 2010-2012, Christian Halstrick * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -76,6 +76,20 @@ public boolean isSuccessful() { return true; } }, + /** + * @since 2.0 + */ + FAST_FORWARD_SQUASHED { + @Override + public String toString() { + return "Fast-forward-squashed"; + } + + @Override + public boolean isSuccessful() { + return true; + } + }, /** */ ALREADY_UP_TO_DATE { @Override @@ -112,6 +126,20 @@ public boolean isSuccessful() { return true; } }, + /** + * @since 2.0 + */ + MERGED_SQUASHED { + @Override + public String toString() { + return "Merged-squashed"; + } + + @Override + public boolean isSuccessful() { + return true; + } + }, /** */ CONFLICTING { @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index b34b902ce..fc3d147c6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Chris Aniszczyk + * Copyright (C) 2011-2012, Chris Aniszczyk * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -221,6 +221,8 @@ public Ref call() throws GitAPIException, CheckoutConflictException { resetMerge(); else if (cherryPicking) resetCherryPick(); + else if (repo.readSquashCommitMsg() != null) + repo.writeSquashCommitMsg(null /* delete */); } setCallable(false); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index aca9574a5..539f83756 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -484,6 +484,7 @@ public static JGitText get() { /***/ public String sourceIsNotAWildcard; /***/ public String sourceRefDoesntResolveToAnyObject; /***/ public String sourceRefNotSpecifiedForRefspec; + /***/ public String squashCommitNotUpdatingHEAD; /***/ public String staleRevFlagsOn; /***/ public String startingReadStageWithoutWrittenRequestDataPendingIsNotSupported; /***/ public String stashApplyFailed; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 5332ffa71..d5be3157d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2008, Google Inc. * Copyright (C) 2008, Robin Rosenberg - * Copyright (C) 2006-2008, Shawn O. Pearce + * Copyright (C) 2006-2012, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -553,6 +553,9 @@ public static byte[] encode(final String str) { /** name of the file containing the ID of a cherry pick commit in case of conflicts */ public static final String CHERRY_PICK_HEAD = "CHERRY_PICK_HEAD"; + /** name of the file containing the commit msg for a squash commit */ + public static final String SQUASH_MSG = "SQUASH_MSG"; + /** * name of the ref ORIG_HEAD used by certain commands to store the original * value of HEAD diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 7b9d453aa..2c04a74f4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -2,7 +2,7 @@ * Copyright (C) 2007, Dave Watson * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2006-2010, Robin Rosenberg - * Copyright (C) 2006-2008, Shawn O. Pearce + * Copyright (C) 2006-2012, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -1125,24 +1125,14 @@ public abstract ReflogReader getReflogReader(String refName) * See {@link #isBare()}. */ public String readMergeCommitMsg() throws IOException, NoWorkTreeException { - if (isBare() || getDirectory() == null) - throw new NoWorkTreeException(); - - File mergeMsgFile = new File(getDirectory(), Constants.MERGE_MSG); - try { - return RawParseUtils.decode(IO.readFully(mergeMsgFile)); - } catch (FileNotFoundException e) { - // MERGE_MSG file has disappeared in the meantime - // ignore it - return null; - } + return readCommitMsgFile(Constants.MERGE_MSG); } /** * Write new content to the file $GIT_DIR/MERGE_MSG. In this file operations * triggering a merge will store a template for the commit message of the * merge commit. If null is specified as message the file will - * be deleted + * be deleted. * * @param msg * the message which should be written or null to @@ -1152,16 +1142,7 @@ public String readMergeCommitMsg() throws IOException, NoWorkTreeException { */ public void writeMergeCommitMsg(String msg) throws IOException { File mergeMsgFile = new File(gitDir, Constants.MERGE_MSG); - if (msg != null) { - FileOutputStream fos = new FileOutputStream(mergeMsgFile); - try { - fos.write(msg.getBytes(Constants.CHARACTER_ENCODING)); - } finally { - fos.close(); - } - } else { - FileUtils.delete(mergeMsgFile, FileUtils.SKIP_MISSING); - } + writeCommitMsg(mergeMsgFile, msg); } /** @@ -1169,9 +1150,9 @@ public void writeMergeCommitMsg(String msg) throws IOException { * file operations triggering a merge will store the IDs of all heads which * should be merged together with HEAD. * - * @return a list of commits which IDs are listed in the MERGE_HEAD - * file or {@code null} if this file doesn't exist. Also if the file - * exists but is empty {@code null} will be returned + * @return a list of commits which IDs are listed in the MERGE_HEAD file or + * {@code null} if this file doesn't exist. Also if the file exists + * but is empty {@code null} will be returned * @throws IOException * @throws NoWorkTreeException * if this is bare, which implies it has no working directory. @@ -1280,6 +1261,65 @@ public ObjectId readOrigHead() throws IOException, NoWorkTreeException { return raw != null ? ObjectId.fromString(raw, 0) : null; } + /** + * Return the information stored in the file $GIT_DIR/SQUASH_MSG. In this + * file operations triggering a squashed merge will store a template for the + * commit message of the squash commit. + * + * @return a String containing the content of the SQUASH_MSG file or + * {@code null} if this file doesn't exist + * @throws IOException + * @throws NoWorkTreeException + * if this is bare, which implies it has no working directory. + * See {@link #isBare()}. + */ + public String readSquashCommitMsg() throws IOException { + return readCommitMsgFile(Constants.SQUASH_MSG); + } + + /** + * Write new content to the file $GIT_DIR/SQUASH_MSG. In this file + * operations triggering a squashed merge will store a template for the + * commit message of the squash commit. If null is specified as + * message the file will be deleted. + * + * @param msg + * the message which should be written or null to + * delete the file + * + * @throws IOException + */ + public void writeSquashCommitMsg(String msg) throws IOException { + File squashMsgFile = new File(gitDir, Constants.SQUASH_MSG); + writeCommitMsg(squashMsgFile, msg); + } + + private String readCommitMsgFile(String msgFilename) throws IOException { + if (isBare() || getDirectory() == null) + throw new NoWorkTreeException(); + + File mergeMsgFile = new File(getDirectory(), msgFilename); + try { + return RawParseUtils.decode(IO.readFully(mergeMsgFile)); + } catch (FileNotFoundException e) { + // the file has disappeared in the meantime ignore it + return null; + } + } + + private void writeCommitMsg(File msgFile, String msg) throws IOException { + if (msg != null) { + FileOutputStream fos = new FileOutputStream(msgFile); + try { + fos.write(msg.getBytes(Constants.CHARACTER_ENCODING)); + } finally { + fos.close(); + } + } else { + FileUtils.delete(msgFile, FileUtils.SKIP_MISSING); + } + } + /** * Read a file from the git directory. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java index 1a26ecfe1..cacaff4c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Robin Stocker + * Copyright (C) 2010-2012, Robin Stocker * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -134,7 +134,7 @@ else if (ref.getName().equals(ref.getObjectId().getName())) public String formatWithConflicts(String message, List conflictingPaths) { StringBuilder sb = new StringBuilder(message); - if (!message.endsWith("\n")) + if (!message.endsWith("\n") && message.length() != 0) sb.append("\n"); sb.append("\n"); sb.append("Conflicts:\n"); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/SquashMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/SquashMessageFormatter.java new file mode 100644 index 000000000..6d0ff9ba9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/SquashMessageFormatter.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012, IBM Corporation and others. + * 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.util.List; + +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.GitDateFormatter; +import org.eclipse.jgit.util.GitDateFormatter.Format; + +/** + * Formatter for constructing the commit message for a squashed commit. + *

+ * The format should be the same as C Git does it, for compatibility. + */ +public class SquashMessageFormatter { + + private GitDateFormatter dateFormatter; + + /** + * Create a new squash message formatter. + */ + public SquashMessageFormatter() { + dateFormatter = new GitDateFormatter(Format.DEFAULT); + } + /** + * Construct the squashed commit message. + * + * @param squashedCommits + * the squashed commits + * @param target + * the target branch + * @return squashed commit message + */ + public String format(List squashedCommits, Ref target) { + StringBuilder sb = new StringBuilder(); + sb.append("Squashed commit of the following:\n"); + for (RevCommit c : squashedCommits) { + sb.append("\ncommit "); + sb.append(c.getName()); + sb.append("\n"); + sb.append(toString(c.getAuthorIdent())); + sb.append("\n\t"); + sb.append(c.getShortMessage()); + sb.append("\n"); + } + return sb.toString(); + } + + private String toString(PersonIdent author) { + final StringBuilder a = new StringBuilder(); + + a.append("Author: "); + a.append(author.getName()); + a.append(" <"); + a.append(author.getEmailAddress()); + a.append(">\n"); + a.append("Date: "); + a.append(dateFormatter.formatDate(author)); + a.append("\n"); + + return a.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java index 50b222bcc..94400b06e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Robin Stocker + * Copyright (C) 2011-2012, Robin Stocker * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -44,6 +44,8 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -83,14 +85,43 @@ private RevWalkUtils() { public static int count(final RevWalk walk, final RevCommit start, final RevCommit end) throws MissingObjectException, IncorrectObjectTypeException, IOException { + return find(walk, start, end).size(); + } + + /** + * Find commits that are reachable from start until a commit + * that is reachable from end is encountered. In other words, + * Find of commits that are in start, but not in + * end. + *

+ * Note that this method calls {@link RevWalk#reset()} at the beginning. + * Also note that the existing rev filter on the walk is left as-is, so be + * sure to set the right rev filter before calling this method. + * + * @param walk + * the rev walk to use + * @param start + * the commit to start counting from + * @param end + * the commit where counting should end, or null if counting + * should be done until there are no more commits + * @return the commits found + * @throws MissingObjectException + * @throws IncorrectObjectTypeException + * @throws IOException + */ + public static List find(final RevWalk walk, + final RevCommit start, final RevCommit end) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { walk.reset(); walk.markStart(start); if (end != null) walk.markUninteresting(end); - int count = 0; - for (RevCommit c = walk.next(); c != null; c = walk.next()) - count++; - return count; + List commits = new ArrayList(); + for (RevCommit c : walk) + commits.add(c); + return commits; } }