/* * Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@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.api; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.eclipse.jgit.api.MergeResult.MergeStatus; import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler; import org.eclipse.jgit.api.RebaseCommand.Operation; import org.eclipse.jgit.api.RebaseResult.Status; import org.eclipse.jgit.api.errors.InvalidRebaseStepException; import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.errors.IllegalTodoFileModification; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.ConfigConstants; 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.RebaseTodoLine; import org.eclipse.jgit.lib.RebaseTodoLine.Action; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; import org.junit.Before; import org.junit.Test; public class RebaseCommandTest extends RepositoryTestCase { private static final String GIT_REBASE_TODO = "rebase-merge/git-rebase-todo"; private static final String FILE1 = "file1"; protected Git git; @Override @Before public void setUp() throws Exception { super.setUp(); this.git = new Git(db); } private void checkoutCommit(RevCommit commit) throws IllegalStateException, IOException { RevCommit head; try (RevWalk walk = new RevWalk(db)) { head = walk.parseCommit(db.resolve(Constants.HEAD)); DirCacheCheckout dco = new DirCacheCheckout(db, head.getTree(), db.lockDirCache(), commit.getTree()); dco.setFailOnConflict(true); dco.checkout(); } // update the HEAD RefUpdate refUpdate = db.updateRef(Constants.HEAD, true); refUpdate.setNewObjectId(commit); refUpdate.setRefLogMessage("checkout: moving to " + head.getName(), false); refUpdate.forceUpdate(); } @Test public void testFastForwardWithNewFile() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit first = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create a topic branch createBranch(first, "refs/heads/topic"); // create file2 on master File file2 = writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); RevCommit second = git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); checkoutBranch("refs/heads/topic"); assertFalse(new File(db.getWorkTree(), "file2").exists()); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); checkFile(file2, "file2"); assertEquals(Status.FAST_FORWARD, res.getStatus()); List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) .getReverseEntries(); List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") .getReverseEntries(); List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); assertEquals("checkout: moving from topic to " + second.getName(), headLog.get(1).getComment()); assertEquals(2, masterLog.size()); assertEquals(2, topicLog.size()); assertEquals( "rebase finished: refs/heads/topic onto " + second.getName(), topicLog.get(0).getComment()); } @Test public void testFastForwardWithMultipleCommits() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit first = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create a topic branch createBranch(first, "refs/heads/topic"); // create file2 on master File file2 = writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // write a second commit writeTrashFile("file2", "file2 new content"); git.add().addFilepattern("file2").call(); RevCommit second = git.commit().setMessage("Change content of file2") .call(); checkoutBranch("refs/heads/topic"); assertFalse(new File(db.getWorkTree(), "file2").exists()); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); checkFile(file2, "file2 new content"); assertEquals(Status.FAST_FORWARD, res.getStatus()); List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) .getReverseEntries(); List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") .getReverseEntries(); List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); assertEquals("checkout: moving from topic to " + second.getName(), headLog.get(1).getComment()); assertEquals(3, masterLog.size()); assertEquals(2, topicLog.size()); assertEquals( "rebase finished: refs/heads/topic onto " + second.getName(), topicLog.get(0).getComment()); } /** * Create the following commits and then attempt to rebase topic onto * master. This will serialize the branches. * * <pre> * A - B (master) * \ * C - D - F (topic) * \ / * E - (side) * </pre> * * into * * <pre> * A - B - (master) C' - D' - E' (topic') * \ * C - D - F (topic) * \ / * E - (side) * </pre> * * @throws Exception */ @Test public void testRebaseShouldIgnoreMergeCommits() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit a = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create a topic branch createBranch(a, "refs/heads/topic"); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); RevCommit b = git.commit().setMessage("updated file1 on master").call(); checkoutBranch("refs/heads/topic"); writeTrashFile("file3", "more changess"); git.add().addFilepattern("file3").call(); RevCommit c = git.commit() .setMessage("update file3 on topic").call(); // create a branch from the topic commit createBranch(c, "refs/heads/side"); // second commit on topic writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); RevCommit d = git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // switch to side branch and update file2 checkoutBranch("refs/heads/side"); writeTrashFile("file3", "more change"); git.add().addFilepattern("file3").call(); RevCommit e = git.commit().setMessage("update file2 on side") .call(); // switch back to topic and merge in side, creating f checkoutBranch("refs/heads/topic"); MergeResult result = git.merge().include(e.getId()) .setStrategy(MergeStrategy.RESOLVE).call(); assertEquals(MergeStatus.MERGED, result.getMergeStatus()); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.OK, res.getStatus()); try (RevWalk rw = new RevWalk(db)) { rw.markStart(rw.parseCommit(db.resolve("refs/heads/topic"))); assertDerivedFrom(rw.next(), e); assertDerivedFrom(rw.next(), d); assertDerivedFrom(rw.next(), c); assertEquals(b, rw.next()); assertEquals(a, rw.next()); } List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) .getReverseEntries(); List<ReflogEntry> sideLog = db.getReflogReader("refs/heads/side") .getReverseEntries(); List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") .getReverseEntries(); List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); assertEquals("rebase: update file2 on side", headLog.get(1) .getComment()); assertEquals("rebase: Add file2", headLog.get(2).getComment()); assertEquals("rebase: update file3 on topic", headLog.get(3) .getComment()); assertEquals("checkout: moving from topic to " + b.getName(), headLog .get(4).getComment()); assertEquals(2, masterLog.size()); assertEquals(2, sideLog.size()); assertEquals(5, topicLog.size()); assertEquals("rebase finished: refs/heads/topic onto " + b.getName(), topicLog.get(0).getComment()); } static void assertDerivedFrom(RevCommit derived, RevCommit original) { assertThat(derived, not(equalTo(original))); assertEquals(original.getFullMessage(), derived.getFullMessage()); } @Test public void testRebasePreservingMerges1() throws Exception { doTestRebasePreservingMerges(true); } @Test public void testRebasePreservingMerges2() throws Exception { doTestRebasePreservingMerges(false); } /** * Transforms the same before-state as in * {@link #testRebaseShouldIgnoreMergeCommits()} to the following. * <p> * This test should always rewrite E. * * <pre> * A - B (master) - - - C' - D' - F' (topic') * \ \ / * C - D - F (topic) - E' * \ / * - E (side) * </pre> * * @param testConflict * @throws Exception */ private void doTestRebasePreservingMerges(boolean testConflict) throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit a = git.commit().setMessage("commit a").call(); // create a topic branch createBranch(a, "refs/heads/topic"); // update FILE1 on master writeTrashFile(FILE1, "blah"); writeTrashFile("conflict", "b"); git.add().addFilepattern(".").call(); RevCommit b = git.commit().setMessage("commit b").call(); checkoutBranch("refs/heads/topic"); writeTrashFile("file3", "more changess"); git.add().addFilepattern("file3").call(); RevCommit c = git.commit().setMessage("commit c").call(); // create a branch from the topic commit createBranch(c, "refs/heads/side"); // second commit on topic writeTrashFile("file2", "file2"); if (testConflict) writeTrashFile("conflict", "d"); git.add().addFilepattern(".").call(); RevCommit d = git.commit().setMessage("commit d").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // switch to side branch and update file2 checkoutBranch("refs/heads/side"); writeTrashFile("file3", "more change"); if (testConflict) writeTrashFile("conflict", "e"); git.add().addFilepattern(".").call(); RevCommit e = git.commit().setMessage("commit e").call(); // switch back to topic and merge in side, creating f checkoutBranch("refs/heads/topic"); MergeResult result = git.merge().include(e.getId()) .setStrategy(MergeStrategy.RESOLVE).call(); final RevCommit f; if (testConflict) { assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); assertEquals(Collections.singleton("conflict"), git.status().call() .getConflicting()); // resolve writeTrashFile("conflict", "f resolved"); git.add().addFilepattern("conflict").call(); f = git.commit().setMessage("commit f").call(); } else { assertEquals(MergeStatus.MERGED, result.getMergeStatus()); try (RevWalk rw = new RevWalk(db)) { f = rw.parseCommit(result.getNewHead()); } } RebaseResult res = git.rebase().setUpstream("refs/heads/master") .setPreserveMerges(true).call(); if (testConflict) { // first there is a conflict whhen applying d assertEquals(Status.STOPPED, res.getStatus()); assertEquals(Collections.singleton("conflict"), git.status().call() .getConflicting()); assertTrue(read("conflict").contains("\nb\n=======\nd\n")); // resolve writeTrashFile("conflict", "d new"); git.add().addFilepattern("conflict").call(); res = git.rebase().setOperation(Operation.CONTINUE).call(); // then there is a conflict when applying e assertEquals(Status.STOPPED, res.getStatus()); assertEquals(Collections.singleton("conflict"), git.status().call() .getConflicting()); assertTrue(read("conflict").contains("\nb\n=======\ne\n")); // resolve writeTrashFile("conflict", "e new"); git.add().addFilepattern("conflict").call(); res = git.rebase().setOperation(Operation.CONTINUE).call(); // finally there is a conflict merging e' assertEquals(Status.STOPPED, res.getStatus()); assertEquals(Collections.singleton("conflict"), git.status().call() .getConflicting()); assertTrue(read("conflict").contains("\nd new\n=======\ne new\n")); // resolve writeTrashFile("conflict", "f new resolved"); git.add().addFilepattern("conflict").call(); res = git.rebase().setOperation(Operation.CONTINUE).call(); } assertEquals(Status.OK, res.getStatus()); if (testConflict) assertEquals("f new resolved", read("conflict")); assertEquals("blah", read(FILE1)); assertEquals("file2", read("file2")); assertEquals("more change", read("file3")); try (RevWalk rw = new RevWalk(db)) { rw.markStart(rw.parseCommit(db.resolve("refs/heads/topic"))); RevCommit newF = rw.next(); assertDerivedFrom(newF, f); assertEquals(2, newF.getParentCount()); RevCommit newD = rw.next(); assertDerivedFrom(newD, d); if (testConflict) assertEquals("d new", readFile("conflict", newD)); RevCommit newE = rw.next(); assertDerivedFrom(newE, e); if (testConflict) assertEquals("e new", readFile("conflict", newE)); assertEquals(newD, newF.getParent(0)); assertEquals(newE, newF.getParent(1)); assertDerivedFrom(rw.next(), c); assertEquals(b, rw.next()); assertEquals(a, rw.next()); } } private String readFile(String path, RevCommit commit) throws IOException { try (TreeWalk walk = TreeWalk.forPath(db, path, commit.getTree())) { ObjectLoader loader = db.open(walk.getObjectId(0), Constants.OBJ_BLOB); String result = RawParseUtils.decode(loader.getCachedBytes()); return result; } } @Test public void testRebasePreservingMergesWithUnrelatedSide1() throws Exception { doTestRebasePreservingMergesWithUnrelatedSide(true); } @Test public void testRebasePreservingMergesWithUnrelatedSide2() throws Exception { doTestRebasePreservingMergesWithUnrelatedSide(false); } /** * Rebase topic onto master, not rewriting E. The merge resulting in D is * confliicting to show that the manual merge resolution survives the * rebase. * * <pre> * A - B - G (master) * \ \ * \ C - D - F (topic) * \ / * E (side) * </pre> * * <pre> * A - B - G (master) * \ \ * \ C' - D' - F' (topic') * \ / * E (side) * </pre> * * @param testConflict * @throws Exception */ private void doTestRebasePreservingMergesWithUnrelatedSide( boolean testConflict) throws Exception { try (RevWalk rw = new RevWalk(db)) { rw.sort(RevSort.TOPO); writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit a = git.commit().setMessage("commit a").call(); writeTrashFile("file2", "blah"); git.add().addFilepattern("file2").call(); RevCommit b = git.commit().setMessage("commit b").call(); // create a topic branch createBranch(b, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file3", "more changess"); writeTrashFile(FILE1, "preparing conflict"); git.add().addFilepattern("file3").addFilepattern(FILE1).call(); RevCommit c = git.commit().setMessage("commit c").call(); createBranch(a, "refs/heads/side"); checkoutBranch("refs/heads/side"); writeTrashFile("conflict", "e"); writeTrashFile(FILE1, FILE1 + "\n" + "line 2"); git.add().addFilepattern(".").call(); RevCommit e = git.commit().setMessage("commit e").call(); // switch back to topic and merge in side, creating d checkoutBranch("refs/heads/topic"); MergeResult result = git.merge().include(e) .setStrategy(MergeStrategy.RESOLVE).call(); assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); assertEquals(result.getConflicts().keySet(), Collections.singleton(FILE1)); writeTrashFile(FILE1, "merge resolution"); git.add().addFilepattern(FILE1).call(); RevCommit d = git.commit().setMessage("commit d").call(); RevCommit f = commitFile("file2", "new content two", "topic"); checkoutBranch("refs/heads/master"); writeTrashFile("fileg", "fileg"); if (testConflict) writeTrashFile("conflict", "g"); git.add().addFilepattern(".").call(); RevCommit g = git.commit().setMessage("commit g").call(); checkoutBranch("refs/heads/topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master") .setPreserveMerges(true).call(); if (testConflict) { assertEquals(Status.STOPPED, res.getStatus()); assertEquals(Collections.singleton("conflict"), git.status().call() .getConflicting()); // resolve writeTrashFile("conflict", "e"); git.add().addFilepattern("conflict").call(); res = git.rebase().setOperation(Operation.CONTINUE).call(); } assertEquals(Status.OK, res.getStatus()); assertEquals("merge resolution", read(FILE1)); assertEquals("new content two", read("file2")); assertEquals("more changess", read("file3")); assertEquals("fileg", read("fileg")); rw.markStart(rw.parseCommit(db.resolve("refs/heads/topic"))); RevCommit newF = rw.next(); assertDerivedFrom(newF, f); RevCommit newD = rw.next(); assertDerivedFrom(newD, d); assertEquals(2, newD.getParentCount()); RevCommit newC = rw.next(); assertDerivedFrom(newC, c); RevCommit newE = rw.next(); assertEquals(e, newE); assertEquals(newC, newD.getParent(0)); assertEquals(e, newD.getParent(1)); assertEquals(g, rw.next()); assertEquals(b, rw.next()); assertEquals(a, rw.next()); } } @Test public void testRebaseParentOntoHeadShouldBeUptoDate() throws Exception { writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit parent = git.commit().setMessage("parent comment").call(); writeTrashFile(FILE1, "another change"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("head commit").call(); RebaseResult result = git.rebase().setUpstream(parent).call(); assertEquals(Status.UP_TO_DATE, result.getStatus()); assertEquals(2, db.getReflogReader(Constants.HEAD).getReverseEntries() .size()); assertEquals(2, db.getReflogReader("refs/heads/master") .getReverseEntries().size()); } @Test public void testUpToDate() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit first = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); RebaseResult res = git.rebase().setUpstream(first).call(); assertEquals(Status.UP_TO_DATE, res.getStatus()); assertEquals(1, db.getReflogReader(Constants.HEAD).getReverseEntries() .size()); assertEquals(1, db.getReflogReader("refs/heads/master") .getReverseEntries().size()); } @Test public void testUnknownUpstream() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); try { git.rebase().setUpstream("refs/heads/xyz").call(); fail("expected exception was not thrown"); } catch (RefNotFoundException e) { // expected exception } } @Test public void testConflictFreeWithSingleFile() throws Exception { // create file1 on master File theFile = writeTrashFile(FILE1, "1\n2\n3\n"); git.add().addFilepattern(FILE1).call(); RevCommit second = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change first line in master and commit writeTrashFile(FILE1, "1master\n2\n3\n"); checkFile(theFile, "1master\n2\n3\n"); git.add().addFilepattern(FILE1).call(); RevCommit lastMasterChange = git.commit().setMessage( "change file1 in master").call(); // create a topic branch based on second commit createBranch(second, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(theFile, "1\n2\n3\n"); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change third line in topic branch writeTrashFile(FILE1, "1\n2\n3\ntopic\n"); git.add().addFilepattern(FILE1).call(); RevCommit origHead = git.commit().setMessage("change file1 in topic") .call(); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.OK, res.getStatus()); checkFile(theFile, "1master\n2\n3\ntopic\n"); // our old branch should be checked out again assertEquals("refs/heads/topic", db.getFullBranch()); try (RevWalk rw = new RevWalk(db)) { assertEquals(lastMasterChange, rw.parseCommit( db.resolve(Constants.HEAD)).getParent(0)); } assertEquals(origHead, db.readOrigHead()); List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) .getReverseEntries(); List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") .getReverseEntries(); List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals(2, masterLog.size()); assertEquals(3, topicLog.size()); assertEquals("rebase finished: refs/heads/topic onto " + lastMasterChange.getName(), topicLog.get(0).getComment()); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); } @Test public void testDetachedHead() throws Exception { // create file1 on master File theFile = writeTrashFile(FILE1, "1\n2\n3\n"); git.add().addFilepattern(FILE1).call(); RevCommit second = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change first line in master and commit writeTrashFile(FILE1, "1master\n2\n3\n"); checkFile(theFile, "1master\n2\n3\n"); git.add().addFilepattern(FILE1).call(); RevCommit lastMasterChange = git.commit().setMessage( "change file1 in master").call(); // create a topic branch based on second commit createBranch(second, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(theFile, "1\n2\n3\n"); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change third line in topic branch writeTrashFile(FILE1, "1\n2\n3\ntopic\n"); git.add().addFilepattern(FILE1).call(); RevCommit topicCommit = git.commit() .setMessage("change file1 in topic").call(); checkoutBranch("refs/heads/master"); checkoutCommit(topicCommit); assertEquals(topicCommit.getId().getName(), db.getFullBranch()); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.OK, res.getStatus()); checkFile(theFile, "1master\n2\n3\ntopic\n"); try (RevWalk rw = new RevWalk(db)) { assertEquals(lastMasterChange, rw.parseCommit( db.resolve(Constants.HEAD)).getParent(0)); } List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) .getReverseEntries(); assertEquals(8, headLog.size()); assertEquals("rebase: change file1 in topic", headLog.get(0) .getComment()); assertEquals("checkout: moving from " + topicCommit.getName() + " to " + lastMasterChange.getName(), headLog.get(1).getComment()); } @Test public void testFilesAddedFromTwoBranches() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit masterCommit = git.commit().setMessage("Add file1 to master") .call(); // create a branch named file2 and add file2 createBranch(masterCommit, "refs/heads/file2"); checkoutBranch("refs/heads/file2"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); RevCommit addFile2 = git.commit().setMessage( "Add file2 to branch file2").call(); // create a branch named file3 and add file3 createBranch(masterCommit, "refs/heads/file3"); checkoutBranch("refs/heads/file3"); writeTrashFile("file3", "file3"); git.add().addFilepattern("file3").call(); git.commit().setMessage("Add file3 to branch file3").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); assertFalse(new File(db.getWorkTree(), "file2").exists()); assertTrue(new File(db.getWorkTree(), "file3").exists()); RebaseResult res = git.rebase().setUpstream("refs/heads/file2").call(); assertEquals(Status.OK, res.getStatus()); assertTrue(new File(db.getWorkTree(), FILE1).exists()); assertTrue(new File(db.getWorkTree(), "file2").exists()); assertTrue(new File(db.getWorkTree(), "file3").exists()); // our old branch should be checked out again assertEquals("refs/heads/file3", db.getFullBranch()); try (RevWalk rw = new RevWalk(db)) { assertEquals(addFile2, rw.parseCommit( db.resolve(Constants.HEAD)).getParent(0)); } checkoutBranch("refs/heads/file2"); assertTrue(new File(db.getWorkTree(), FILE1).exists()); assertTrue(new File(db.getWorkTree(), "file2").exists()); assertFalse(new File(db.getWorkTree(), "file3").exists()); } @Test public void testStopOnConflict() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change first line in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on second commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "topic4"); // change first line (conflicting) RevCommit conflicting = writeFileAndCommit(FILE1, "change file1 in topic", "1topic", "2", "3", "topic4"); RevCommit lastTopicCommit = writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2", "3", "topic4"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); assertEquals(conflicting, res.getCurrentCommit()); checkFile(FILE1, "<<<<<<< Upstream, based on master\n1master\n=======\n1topic", ">>>>>>> e0d1dea change file1 in topic\n2\n3\ntopic4"); assertEquals(RepositoryState.REBASING_MERGE, db .getRepositoryState()); assertTrue(new File(db.getDirectory(), "rebase-merge").exists()); // the first one should be included, so we should have left two picks in // the file assertEquals(1, countPicks()); // rebase should not succeed in this state try { git.rebase().setUpstream("refs/heads/master").call(); fail("Expected exception was not thrown"); } catch (WrongRepositoryStateException e) { // expected } // abort should reset to topic branch res = git.rebase().setOperation(Operation.ABORT).call(); assertEquals(res.getStatus(), Status.ABORTED); assertEquals("refs/heads/topic", db.getFullBranch()); checkFile(FILE1, "1topic", "2", "3", "topic4"); try (RevWalk rw = new RevWalk(db)) { assertEquals(lastTopicCommit, rw.parseCommit(db.resolve(Constants.HEAD))); } assertEquals(RepositoryState.SAFE, db.getRepositoryState()); // rebase- dir in .git must be deleted assertFalse(new File(db.getDirectory(), "rebase-merge").exists()); } @Test public void testStopOnConflictAndAbortWithDetachedHEAD() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change first line in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on second commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "topic4"); // change first line (conflicting) RevCommit conflicting = writeFileAndCommit(FILE1, "change file1 in topic", "1topic", "2", "3", "topic4"); RevCommit lastTopicCommit = writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2", "3", "topic4"); git.checkout().setName(lastTopicCommit.getName()).call(); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); assertEquals(conflicting, res.getCurrentCommit()); checkFile(FILE1, "<<<<<<< Upstream, based on master\n1master\n=======\n1topic", ">>>>>>> e0d1dea change file1 in topic\n2\n3\ntopic4"); assertEquals(RepositoryState.REBASING_MERGE, db.getRepositoryState()); assertTrue(new File(db.getDirectory(), "rebase-merge").exists()); // the first one should be included, so we should have left two picks in // the file assertEquals(1, countPicks()); // rebase should not succeed in this state try { git.rebase().setUpstream("refs/heads/master").call(); fail("Expected exception was not thrown"); } catch (WrongRepositoryStateException e) { // expected } // abort should reset to topic branch res = git.rebase().setOperation(Operation.ABORT).call(); assertEquals(res.getStatus(), Status.ABORTED); assertEquals(lastTopicCommit.getName(), db.getFullBranch()); checkFile(FILE1, "1topic", "2", "3", "topic4"); try (RevWalk rw = new RevWalk(db)) { assertEquals(lastTopicCommit, rw.parseCommit(db.resolve(Constants.HEAD))); } assertEquals(RepositoryState.SAFE, db.getRepositoryState()); // rebase- dir in .git must be deleted assertFalse(new File(db.getDirectory(), "rebase-merge").exists()); } @Test public void testStopOnConflictAndContinue() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); // change second line (not conflicting) writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2topic", "3", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); // continue should throw a meaningful exception try { res = git.rebase().setOperation(Operation.CONTINUE).call(); fail("Expected Exception not thrown"); } catch (UnmergedPathsException e) { // expected } // merge the file; the second topic commit should go through writeFileAndAdd(FILE1, "1topic", "2", "3", "4topic"); res = git.rebase().setOperation(Operation.CONTINUE).call(); assertNotNull(res); assertEquals(Status.OK, res.getStatus()); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); ObjectId headId = db.resolve(Constants.HEAD); try (RevWalk rw = new RevWalk(db)) { RevCommit rc = rw.parseCommit(headId); RevCommit parent = rw.parseCommit(rc.getParent(0)); assertEquals("change file1 in topic\n\nThis is conflicting", parent .getFullMessage()); } } @Test public void testStopOnConflictAndContinueWithNoDeltaToMaster() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); // continue should throw a meaningful exception try { res = git.rebase().setOperation(Operation.CONTINUE).call(); fail("Expected Exception not thrown"); } catch (UnmergedPathsException e) { // expected } // merge the file; the second topic commit should go through writeFileAndAdd(FILE1, "1master", "2", "3"); res = git.rebase().setOperation(Operation.CONTINUE).call(); assertNotNull(res); assertEquals(Status.NOTHING_TO_COMMIT, res.getStatus()); assertEquals(RepositoryState.REBASING_MERGE, db.getRepositoryState()); git.rebase().setOperation(Operation.SKIP).call(); ObjectId headId = db.resolve(Constants.HEAD); try (RevWalk rw = new RevWalk(db)) { RevCommit rc = rw.parseCommit(headId); assertEquals("change file1 in master", rc.getFullMessage()); } } @Test public void testStopOnConflictAndFailContinueIfFileIsDirty() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); // change second line (not conflicting) writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2topic", "3", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); git.add().addFilepattern(FILE1).call(); File trashFile = writeTrashFile(FILE1, "Some local change"); res = git.rebase().setOperation(Operation.CONTINUE).call(); assertNotNull(res); assertEquals(Status.STOPPED, res.getStatus()); checkFile(trashFile, "Some local change"); } @Test public void testStopOnLastConflictAndContinue() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); // merge the file; the second topic commit should go through writeFileAndAdd(FILE1, "1topic", "2", "3", "4topic"); res = git.rebase().setOperation(Operation.CONTINUE).call(); assertNotNull(res); assertEquals(Status.OK, res.getStatus()); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testStopOnLastConflictAndSkip() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); // merge the file; the second topic commit should go through writeFileAndAdd(FILE1, "1topic", "2", "3", "4topic"); res = git.rebase().setOperation(Operation.SKIP).call(); assertNotNull(res); assertEquals(Status.OK, res.getStatus()); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testMergeFirstStopOnLastConflictAndSkip() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1topic", "2", "3", "4topic"); // change first line (conflicting again) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topicagain", "2", "3", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); writeFileAndAdd(FILE1, "merged"); res = git.rebase().setOperation(Operation.CONTINUE).call(); assertEquals(Status.STOPPED, res.getStatus()); res = git.rebase().setOperation(Operation.SKIP).call(); assertNotNull(res); assertEquals(Status.OK, res.getStatus()); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); checkFile(FILE1, "merged"); } @Test public void testStopOnConflictAndSkipNoConflict() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); // change third line (not conflicting) writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2", "3topic", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); res = git.rebase().setOperation(Operation.SKIP).call(); checkFile(FILE1, "1master", "2", "3topic", "4topic"); assertEquals(Status.OK, res.getStatus()); } @Test public void testStopOnConflictAndSkipWithConflict() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3", "4"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3master", "4"); checkFile(FILE1, "1master", "2", "3master", "4"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3", "4"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4", "5topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4", "5topic"); // change third line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2", "3topic", "4", "5topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); res = git.rebase().setOperation(Operation.SKIP).call(); // TODO is this correct? It is what the command line returns checkFile( FILE1, "1master\n2\n<<<<<<< Upstream, based on master\n3master\n=======\n3topic", ">>>>>>> 5afc8df change file1 in topic again\n4\n5topic"); assertEquals(Status.STOPPED, res.getStatus()); } @Test public void testStopOnConflictCommitAndContinue() throws Exception { // create file1 on master RevCommit firstInMaster = writeFileAndCommit(FILE1, "Add file1", "1", "2", "3"); // change in master writeFileAndCommit(FILE1, "change file1 in master", "1master", "2", "3"); checkFile(FILE1, "1master", "2", "3"); // create a topic branch based on the first commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); // we have the old content again checkFile(FILE1, "1", "2", "3"); // add a line (non-conflicting) writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", "3", "4topic"); // change first line (conflicting) writeFileAndCommit(FILE1, "change file1 in topic\n\nThis is conflicting", "1topic", "2", "3", "4topic"); // change second line (not conflicting) writeFileAndCommit(FILE1, "change file1 in topic again", "1topic", "2", "3topic", "4topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); // continue should throw a meaningful exception try { res = git.rebase().setOperation(Operation.CONTINUE).call(); fail("Expected Exception not thrown"); } catch (UnmergedPathsException e) { // expected } // merge the file; the second topic commit should go through writeFileAndCommit(FILE1, "A different commit message", "1topic", "2", "3", "4topic"); res = git.rebase().setOperation(Operation.CONTINUE).call(); assertNotNull(res); // nothing to commit. this leaves the repo state in rebase, so that the // user can decide what to do. if he accidentally committed, reset soft, // and continue, if he really has nothing to commit, skip. assertEquals(Status.NOTHING_TO_COMMIT, res.getStatus()); assertEquals(RepositoryState.REBASING_MERGE, db.getRepositoryState()); git.rebase().setOperation(Operation.SKIP).call(); ObjectId headId = db.resolve(Constants.HEAD); try (RevWalk rw = new RevWalk(db)) { RevCommit rc = rw.parseCommit(headId); RevCommit parent = rw.parseCommit(rc.getParent(0)); assertEquals("A different commit message", parent.getFullMessage()); } } private RevCommit writeFileAndCommit(String fileName, String commitMessage, String... lines) throws Exception { StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append(line); sb.append('\n'); } writeTrashFile(fileName, sb.toString()); git.add().addFilepattern(fileName).call(); return git.commit().setMessage(commitMessage).call(); } private void writeFileAndAdd(String fileName, String... lines) throws Exception { StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append(line); sb.append('\n'); } writeTrashFile(fileName, sb.toString()); git.add().addFilepattern(fileName).call(); } private void checkFile(String fileName, String... lines) throws Exception { File file = new File(db.getWorkTree(), fileName); StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append(line); sb.append('\n'); } checkFile(file, sb.toString()); } @Test public void testStopOnConflictFileCreationAndDeletion() throws Exception { // create file1 on master writeTrashFile(FILE1, "Hello World"); git.add().addFilepattern(FILE1).call(); // create file2 on master File file2 = writeTrashFile("file2", "Hello World 2"); git.add().addFilepattern("file2").call(); // create file3 on master File file3 = writeTrashFile("file3", "Hello World 3"); git.add().addFilepattern("file3").call(); RevCommit firstInMaster = git.commit() .setMessage("Add file 1, 2 and 3").call(); // create file4 on master File file4 = writeTrashFile("file4", "Hello World 4"); git.add().addFilepattern("file4").call(); deleteTrashFile("file2"); git.add().setUpdate(true).addFilepattern("file2").call(); // create folder folder6 on topic (conflicts with file folder6 on topic // later on) writeTrashFile("folder6/file1", "Hello World folder6"); git.add().addFilepattern("folder6/file1").call(); git.commit().setMessage( "Add file 4 and folder folder6, delete file2 on master").call(); // create a topic branch based on second commit createBranch(firstInMaster, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); deleteTrashFile("file3"); git.add().setUpdate(true).addFilepattern("file3").call(); // create file5 on topic File file5 = writeTrashFile("file5", "Hello World 5"); git.add().addFilepattern("file5").call(); git.commit().setMessage("Delete file3 and add file5 in topic").call(); // create file folder6 on topic (conflicts with folder6 on master) writeTrashFile("folder6", "Hello World 6"); git.add().addFilepattern("folder6").call(); // create file7 on topic File file7 = writeTrashFile("file7", "Hello World 7"); git.add().addFilepattern("file7").call(); deleteTrashFile("file5"); git.add().setUpdate(true).addFilepattern("file5").call(); RevCommit conflicting = git.commit().setMessage( "Delete file5, add file folder6 and file7 in topic").call(); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.STOPPED, res.getStatus()); assertEquals(conflicting, res.getCurrentCommit()); assertEquals(RepositoryState.REBASING_MERGE, db .getRepositoryState()); assertTrue(new File(db.getDirectory(), "rebase-merge").exists()); // the first one should be included, so we should have left two picks in // the file assertEquals(0, countPicks()); assertFalse(file2.exists()); assertFalse(file3.exists()); assertTrue(file4.exists()); assertFalse(file5.exists()); assertTrue(file7.exists()); // abort should reset to topic branch res = git.rebase().setOperation(Operation.ABORT).call(); assertEquals(res.getStatus(), Status.ABORTED); assertEquals("refs/heads/topic", db.getFullBranch()); try (RevWalk rw = new RevWalk(db)) { assertEquals(conflicting, rw.parseCommit(db.resolve(Constants.HEAD))); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } // rebase- dir in .git must be deleted assertFalse(new File(db.getDirectory(), "rebase-merge").exists()); assertTrue(file2.exists()); assertFalse(file3.exists()); assertFalse(file4.exists()); assertFalse(file5.exists()); assertTrue(file7.exists()); } @Test public void testAuthorScriptConverter() throws Exception { // -1 h timezone offset PersonIdent ident = new PersonIdent("Author name", "a.mail@some.com", 123456789123L, -60); String convertedAuthor = git.rebase().toAuthorScript(ident); String[] lines = convertedAuthor.split("\n"); assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]); assertEquals("GIT_AUTHOR_EMAIL='a.mail@some.com'", lines[1]); assertEquals("GIT_AUTHOR_DATE='@123456789 -0100'", lines[2]); PersonIdent parsedIdent = git.rebase().parseAuthor( convertedAuthor.getBytes("UTF-8")); assertEquals(ident.getName(), parsedIdent.getName()); assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress()); // this is rounded to the last second assertEquals(123456789000L, parsedIdent.getWhen().getTime()); assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset()); // + 9.5h timezone offset ident = new PersonIdent("Author name", "a.mail@some.com", 123456789123L, +570); convertedAuthor = git.rebase().toAuthorScript(ident); lines = convertedAuthor.split("\n"); assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]); assertEquals("GIT_AUTHOR_EMAIL='a.mail@some.com'", lines[1]); assertEquals("GIT_AUTHOR_DATE='@123456789 +0930'", lines[2]); parsedIdent = git.rebase().parseAuthor( convertedAuthor.getBytes("UTF-8")); assertEquals(ident.getName(), parsedIdent.getName()); assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress()); assertEquals(123456789000L, parsedIdent.getWhen().getTime()); assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset()); } @Test public void testRepositoryStateChecks() throws Exception { try { git.rebase().setOperation(Operation.ABORT).call(); fail("Expected Exception not thrown"); } catch (WrongRepositoryStateException e) { // expected } try { git.rebase().setOperation(Operation.SKIP).call(); fail("Expected Exception not thrown"); } catch (WrongRepositoryStateException e) { // expected } try { git.rebase().setOperation(Operation.CONTINUE).call(); fail("Expected Exception not thrown"); } catch (WrongRepositoryStateException e) { // expected } } @Test public void testRebaseWithUntrackedFile() throws Exception { // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / create untracked file3 checkoutBranch("refs/heads/topic"); writeTrashFile("file3", "untracked file3"); // rebase assertEquals(Status.OK, git.rebase().setUpstream("refs/heads/master") .call().getStatus()); } @Test public void testRebaseWithUnstagedTopicChange() throws Exception { // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file2 checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "unstaged file2"); // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus()); assertEquals(1, result.getUncommittedChanges().size()); assertEquals("file2", result.getUncommittedChanges().get(0)); } @Test public void testRebaseWithUncommittedTopicChange() throws Exception { // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file2 and add checkoutBranch("refs/heads/topic"); File uncommittedFile = writeTrashFile("file2", "uncommitted file2"); git.add().addFilepattern("file2").call(); // do not commit RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus()); assertEquals(1, result.getUncommittedChanges().size()); assertEquals("file2", result.getUncommittedChanges().get(0)); checkFile(uncommittedFile, "uncommitted file2"); assertEquals(RepositoryState.SAFE, git.getRepository().getRepositoryState()); } @Test public void testRebaseWithUnstagedMasterChange() throws Exception { // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file1 checkoutBranch("refs/heads/topic"); writeTrashFile(FILE1, "unstaged modified file1"); // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus()); assertEquals(1, result.getUncommittedChanges().size()); assertEquals(FILE1, result.getUncommittedChanges().get(0)); } @Test public void testRebaseWithUncommittedMasterChange() throws Exception { // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file1 and add checkoutBranch("refs/heads/topic"); writeTrashFile(FILE1, "uncommitted modified file1"); git.add().addFilepattern(FILE1).call(); // do not commit // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus()); assertEquals(1, result.getUncommittedChanges().size()); assertEquals(FILE1, result.getUncommittedChanges().get(0)); } @Test public void testRebaseWithUnstagedMasterChangeBaseCommit() throws Exception { // create file0 + file1, add and commit writeTrashFile("file0", "file0"); writeTrashFile(FILE1, "file1"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 checkoutBranch("refs/heads/topic"); writeTrashFile("file0", "unstaged modified file0"); // rebase assertEquals(Status.UNCOMMITTED_CHANGES, git.rebase().setUpstream("refs/heads/master") .call().getStatus()); } @Test public void testRebaseWithUncommittedMasterChangeBaseCommit() throws Exception { // create file0 + file1, add and commit File file0 = writeTrashFile("file0", "file0"); writeTrashFile(FILE1, "file1"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 and add checkoutBranch("refs/heads/topic"); write(file0, "unstaged modified file0"); git.add().addFilepattern("file0").call(); // do not commit // get current index state String indexState = indexState(CONTENT); // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus()); assertEquals(1, result.getUncommittedChanges().size()); // index shall be unchanged assertEquals(indexState, indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testRebaseWithUnstagedMasterChangeOtherCommit() throws Exception { // create file0, add and commit writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("commit0").call(); // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 checkoutBranch("refs/heads/topic"); writeTrashFile("file0", "unstaged modified file0"); // rebase assertEquals(Status.UNCOMMITTED_CHANGES, git.rebase().setUpstream("refs/heads/master") .call().getStatus()); } @Test public void testRebaseWithUncommittedMasterChangeOtherCommit() throws Exception { // create file0, add and commit File file0 = writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("commit0").call(); // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 and add checkoutBranch("refs/heads/topic"); write(file0, "unstaged modified file0"); git.add().addFilepattern("file0").call(); // do not commit // get current index state String indexState = indexState(CONTENT); // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus()); // staged file0 causes DIRTY_INDEX assertEquals(1, result.getUncommittedChanges().size()); assertEquals("unstaged modified file0", read(file0)); // index shall be unchanged assertEquals(indexState, indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testFastForwardRebaseWithModification() throws Exception { // create file0 + file1, add and commit writeTrashFile("file0", "file0"); writeTrashFile(FILE1, "file1"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch createBranch(commit, "refs/heads/topic"); // still on master / modify file1, add and commit writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit2").call(); // checkout topic branch / modify file0 and add to index checkoutBranch("refs/heads/topic"); writeTrashFile("file0", "modified file0 in index"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); // modify once more writeTrashFile("file0", "modified file0"); // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.FAST_FORWARD, result.getStatus()); checkFile(new File(db.getWorkTree(), "file0"), "modified file0"); checkFile(new File(db.getWorkTree(), FILE1), "modified file1"); assertEquals("[file0, mode:100644, content:modified file0 in index]" + "[file1, mode:100644, content:modified file1]", indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testRebaseWithModificationShouldNotDeleteData() throws Exception { // create file0 + file1, add and commit writeTrashFile("file0", "file0"); writeTrashFile(FILE1, "file1"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch createBranch(commit, "refs/heads/topic"); // still on master / modify file1, add and commit writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit2").call(); // checkout topic branch / modify file1, add and commit checkoutBranch("refs/heads/topic"); writeTrashFile(FILE1, "modified file1 on topic"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); writeTrashFile("file0", "modified file0"); RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); // the following condition was true before commit 83b6ab233: // jgit started the rebase and deleted the change on abort // This test should verify that content was deleted if (result.getStatus() == Status.STOPPED) git.rebase().setOperation(Operation.ABORT).call(); checkFile(new File(db.getWorkTree(), "file0"), "modified file0"); checkFile(new File(db.getWorkTree(), FILE1), "modified file1 on topic"); assertEquals("[file0, mode:100644, content:file0]" + "[file1, mode:100644, content:modified file1 on topic]", indexState(CONTENT)); } @Test public void testRebaseWithUncommittedDelete() throws Exception { // create file0 + file1, add and commit File file0 = writeTrashFile("file0", "file0"); writeTrashFile(FILE1, "file1"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch createBranch(commit, "refs/heads/topic"); // still on master / modify file1, add and commit writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit2").call(); // checkout topic branch / delete file0 and add to index checkoutBranch("refs/heads/topic"); git.rm().addFilepattern("file0").call(); // do not commit // rebase RebaseResult result = git.rebase().setUpstream("refs/heads/master") .call(); assertEquals(Status.FAST_FORWARD, result.getStatus()); assertFalse("File should still be deleted", file0.exists()); // index should only have updated file1 assertEquals("[file1, mode:100644, content:modified file1]", indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testRebaseWithAutoStash() throws Exception { // create file0, add and commit db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOSTASH, true); writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("commit0").call(); // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 checkoutBranch("refs/heads/topic"); writeTrashFile("file0", "unstaged modified file0"); // rebase assertEquals(Status.OK, git.rebase().setUpstream("refs/heads/master").call() .getStatus()); checkFile(new File(db.getWorkTree(), "file0"), "unstaged modified file0"); checkFile(new File(db.getWorkTree(), FILE1), "modified file1"); checkFile(new File(db.getWorkTree(), "file2"), "file2"); assertEquals("[file0, mode:100644, content:file0]" + "[file1, mode:100644, content:modified file1]" + "[file2, mode:100644, content:file2]", indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } @Test public void testRebaseWithAutoStashConflictOnApply() throws Exception { // create file0, add and commit db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOSTASH, true); writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("commit0").call(); // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch and checkout / create file2, add and commit createBranch(commit, "refs/heads/topic"); checkoutBranch("refs/heads/topic"); writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("commit2").call(); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 checkoutBranch("refs/heads/topic"); writeTrashFile("file1", "unstaged modified file1"); // rebase assertEquals(Status.STASH_APPLY_CONFLICTS, git.rebase().setUpstream("refs/heads/master").call() .getStatus()); checkFile(new File(db.getWorkTree(), "file0"), "file0"); checkFile( new File(db.getWorkTree(), FILE1), "<<<<<<< HEAD\nmodified file1\n=======\nunstaged modified file1\n>>>>>>> stash\n"); checkFile(new File(db.getWorkTree(), "file2"), "file2"); assertEquals( "[file0, mode:100644, content:file0]" + "[file1, mode:100644, stage:1, content:file1]" + "[file1, mode:100644, stage:2, content:modified file1]" + "[file1, mode:100644, stage:3, content:unstaged modified file1]" + "[file2, mode:100644, content:file2]", indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); List<DiffEntry> diffs = getStashedDiff(); assertEquals(1, diffs.size()); assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType()); assertEquals("file1", diffs.get(0).getOldPath()); } @Test public void testFastForwardRebaseWithAutoStash() throws Exception { // create file0, add and commit db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOSTASH, true); writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("commit0").call(); // create file1, add and commit writeTrashFile(FILE1, "file1"); git.add().addFilepattern(FILE1).call(); RevCommit commit = git.commit().setMessage("commit1").call(); // create topic branch createBranch(commit, "refs/heads/topic"); // checkout master branch / modify file1, add and commit checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // checkout topic branch / modify file0 checkoutBranch("refs/heads/topic"); writeTrashFile("file0", "unstaged modified file0"); // rebase assertEquals(Status.FAST_FORWARD, git.rebase().setUpstream("refs/heads/master") .call().getStatus()); checkFile(new File(db.getWorkTree(), "file0"), "unstaged modified file0"); checkFile(new File(db.getWorkTree(), FILE1), "modified file1"); assertEquals("[file0, mode:100644, content:file0]" + "[file1, mode:100644, content:modified file1]", indexState(CONTENT)); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } private List<DiffEntry> getStashedDiff() throws AmbiguousObjectException, IncorrectObjectTypeException, IOException, MissingObjectException { ObjectId stashId = db.resolve("stash@{0}"); RevWalk revWalk = new RevWalk(db); RevCommit stashCommit = revWalk.parseCommit(stashId); List<DiffEntry> diffs = diffWorkingAgainstHead(stashCommit, revWalk); return diffs; } private TreeWalk createTreeWalk() { TreeWalk walk = new TreeWalk(db); walk.setRecursive(true); walk.setFilter(TreeFilter.ANY_DIFF); return walk; } private List<DiffEntry> diffWorkingAgainstHead(final RevCommit commit, RevWalk revWalk) throws IOException { RevCommit parentCommit = revWalk.parseCommit(commit.getParent(0)); try (TreeWalk walk = createTreeWalk()) { walk.addTree(parentCommit.getTree()); walk.addTree(commit.getTree()); return DiffEntry.scan(walk); } } private int countPicks() throws IOException { int count = 0; File todoFile = getTodoFile(); BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream(todoFile), "UTF-8")); try { String line = br.readLine(); while (line != null) { int firstBlank = line.indexOf(' '); if (firstBlank != -1) { String actionToken = line.substring(0, firstBlank); Action action = null; try { action = Action.parse(actionToken); } catch (Exception e) { // ignore } if (Action.PICK.equals(action)) count++; } line = br.readLine(); } return count; } finally { br.close(); } } @Test public void testFastForwardWithMultipleCommitsOnDifferentBranches() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); RevCommit first = git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create a topic branch createBranch(first, "refs/heads/topic"); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); RevCommit second = git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // create side branch createBranch(second, "refs/heads/side"); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master") .call(); // switch to side branch and update file2 checkoutBranch("refs/heads/side"); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); RevCommit fourth = git.commit().setMessage("update file2 on side") .call(); // switch back to master and merge in side checkoutBranch("refs/heads/master"); MergeResult result = git.merge().include(fourth.getId()) .setStrategy(MergeStrategy.RESOLVE).call(); assertEquals(MergeStatus.MERGED, result.getMergeStatus()); // switch back to topic branch and rebase it onto master checkoutBranch("refs/heads/topic"); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); checkFile(new File(db.getWorkTree(), "file2"), "more change"); assertEquals(Status.FAST_FORWARD, res.getStatus()); } @Test public void testRebaseShouldLeaveWorkspaceUntouchedWithUnstagedChangesConflict() throws Exception { writeTrashFile(FILE1, "initial file"); git.add().addFilepattern(FILE1).call(); RevCommit initial = git.commit().setMessage("initial commit").call(); createBranch(initial, "refs/heads/side"); writeTrashFile(FILE1, "updated file"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated FILE1 on master").call(); // switch to side, modify the file checkoutBranch("refs/heads/side"); writeTrashFile(FILE1, "side update"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated FILE1 on side").call(); File theFile = writeTrashFile(FILE1, "dirty the file"); // and attempt to rebase RebaseResult rebaseResult = git.rebase() .setUpstream("refs/heads/master").call(); assertEquals(Status.UNCOMMITTED_CHANGES, rebaseResult.getStatus()); assertEquals(1, rebaseResult.getUncommittedChanges().size()); assertEquals(FILE1, rebaseResult.getUncommittedChanges().get(0)); checkFile(theFile, "dirty the file"); assertEquals(RepositoryState.SAFE, git.getRepository() .getRepositoryState()); } @Test public void testAbortShouldAlsoAbortNonInteractiveRebaseWithRebaseApplyDir() throws Exception { writeTrashFile(FILE1, "initial file"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("initial commit").call(); File applyDir = new File(db.getDirectory(), "rebase-apply"); File headName = new File(applyDir, "head-name"); FileUtils.mkdir(applyDir); write(headName, "master"); db.writeOrigHead(db.resolve(Constants.HEAD)); git.rebase().setOperation(Operation.ABORT).call(); assertFalse("Abort should clean up .git/rebase-apply", applyDir.exists()); assertEquals(RepositoryState.SAFE, git.getRepository() .getRepositoryState()); } @Test public void testRebaseShouldBeAbleToHandleEmptyLinesInRebaseTodoFile() throws IOException { String emptyLine = "\n"; String todo = "pick 1111111 Commit 1\n" + emptyLine + "pick 2222222 Commit 2\n" + emptyLine + "# Comment line at end\n"; write(getTodoFile(), todo); List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false); assertEquals(2, steps.size()); assertEquals("1111111", steps.get(0).getCommit().name()); assertEquals("2222222", steps.get(1).getCommit().name()); } @Test public void testRebaseShouldBeAbleToHandleLinesWithoutCommitMessageInRebaseTodoFile() throws IOException { String todo = "pick 1111111 \n" + "pick 2222222 Commit 2\n" + "# Comment line at end\n"; write(getTodoFile(), todo); List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false); assertEquals(2, steps.size()); assertEquals("1111111", steps.get(0).getCommit().name()); assertEquals("2222222", steps.get(1).getCommit().name()); } @Test public void testRebaseShouldNotFailIfUserAddCommentLinesInPrepareSteps() throws Exception { commitFile(FILE1, FILE1, "master"); RevCommit c2 = commitFile("file2", "file2", "master"); // update files on master commitFile(FILE1, "blah", "master"); RevCommit c4 = commitFile("file2", "more change", "master"); RebaseResult res = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { steps.add(0, new RebaseTodoLine( "# Comment that should not be processed")); } public String modifyCommitMessage(String commit) { fail("modifyCommitMessage() was not expected to be called"); return commit; } }).call(); assertEquals(RebaseResult.Status.FAST_FORWARD, res.getStatus()); RebaseResult res2 = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { // delete RevCommit c4 steps.get(0).setAction(Action.COMMENT); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { fail("modifyCommitMessage() was not expected to be called"); return commit; } }).call(); assertEquals(RebaseResult.Status.OK, res2.getStatus()); ObjectId headId = db.resolve(Constants.HEAD); try (RevWalk rw = new RevWalk(db)) { RevCommit rc = rw.parseCommit(headId); ObjectId head1Id = db.resolve(Constants.HEAD + "~1"); RevCommit rc1 = rw.parseCommit(head1Id); assertEquals(rc.getFullMessage(), c4.getFullMessage()); assertEquals(rc1.getFullMessage(), c2.getFullMessage()); } } @Test public void testParseRewordCommand() throws Exception { String todo = "pick 1111111 Commit 1\n" + "reword 2222222 Commit 2\n"; write(getTodoFile(), todo); List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false); assertEquals(2, steps.size()); assertEquals("1111111", steps.get(0).getCommit().name()); assertEquals("2222222", steps.get(1).getCommit().name()); assertEquals(Action.REWORD, steps.get(1).getAction()); } @Test public void testEmptyRebaseTodo() throws Exception { write(getTodoFile(), ""); assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, true).size()); assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size()); } @Test public void testOnlyCommentRebaseTodo() throws Exception { write(getTodoFile(), "# a b c d e\n# e f"); assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size()); List<RebaseTodoLine> lines = db.readRebaseTodo(GIT_REBASE_TODO, true); assertEquals(2, lines.size()); for (RebaseTodoLine line : lines) assertEquals(Action.COMMENT, line.getAction()); write(getTodoFile(), "# a b c d e\n# e f\n"); assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size()); lines = db.readRebaseTodo(GIT_REBASE_TODO, true); assertEquals(2, lines.size()); for (RebaseTodoLine line : lines) assertEquals(Action.COMMENT, line.getAction()); write(getTodoFile(), " \r\n# a b c d e\r\n# e f\r\n#"); assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size()); lines = db.readRebaseTodo(GIT_REBASE_TODO, true); assertEquals(4, lines.size()); for (RebaseTodoLine line : lines) assertEquals(Action.COMMENT, line.getAction()); } @Test public void testLeadingSpacesRebaseTodo() throws Exception { String todo = " \t\t pick 1111111 Commit 1\n" + "\t\n" + "\treword 2222222 Commit 2\n"; write(getTodoFile(), todo); List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false); assertEquals(2, steps.size()); assertEquals("1111111", steps.get(0).getCommit().name()); assertEquals("2222222", steps.get(1).getCommit().name()); assertEquals(Action.REWORD, steps.get(1).getAction()); } @Test public void testRebaseShouldTryToParseValidLineMarkedAsComment() throws IOException { String todo = "# pick 1111111 Valid line commented out with space\n" + "#edit 2222222 Valid line commented out without space\n" + "# pick invalidLine Comment line at end\n"; write(getTodoFile(), todo); List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, true); assertEquals(3, steps.size()); RebaseTodoLine firstLine = steps.get(0); assertEquals("1111111", firstLine.getCommit().name()); assertEquals("Valid line commented out with space", firstLine.getShortMessage()); assertEquals("comment", firstLine.getAction().toToken()); try { firstLine.setAction(Action.PICK); assertEquals("1111111", firstLine.getCommit().name()); assertEquals("pick", firstLine.getAction().toToken()); } catch (Exception e) { fail("Valid parsable RebaseTodoLine that has been commented out should allow to change the action, but failed"); } assertEquals("2222222", steps.get(1).getCommit().name()); assertEquals("comment", steps.get(1).getAction().toToken()); assertEquals(null, steps.get(2).getCommit()); assertEquals(null, steps.get(2).getShortMessage()); assertEquals("comment", steps.get(2).getAction().toToken()); assertEquals("# pick invalidLine Comment line at end", steps.get(2) .getComment()); try { steps.get(2).setAction(Action.PICK); fail("A comment RebaseTodoLine that doesn't contain a valid parsable line should fail, but doesn't"); } catch (Exception e) { // expected } } @SuppressWarnings("unused") @Test public void testRebaseTodoLineSetComment() throws Exception { try { new RebaseTodoLine("This is a invalid comment"); fail("Constructing a comment line with invalid comment string should fail, but doesn't"); } catch (IllegalArgumentException e) { // expected } RebaseTodoLine validCommentLine = new RebaseTodoLine( "# This is a comment"); assertEquals(Action.COMMENT, validCommentLine.getAction()); assertEquals("# This is a comment", validCommentLine.getComment()); RebaseTodoLine actionLineToBeChanged = new RebaseTodoLine(Action.EDIT, AbbreviatedObjectId.fromString("1111111"), "short Message"); assertEquals(null, actionLineToBeChanged.getComment()); try { actionLineToBeChanged.setComment("invalid comment"); fail("Setting a invalid comment string should fail but doesn't"); } catch (IllegalArgumentException e) { assertEquals(null, actionLineToBeChanged.getComment()); } actionLineToBeChanged.setComment("# valid comment"); assertEquals("# valid comment", actionLineToBeChanged.getComment()); try { actionLineToBeChanged.setComment("invalid comment"); fail("Setting a invalid comment string should fail but doesn't"); } catch (IllegalArgumentException e) { // expected // setting comment failed, but was successfully set before, // therefore it may not be altered since then assertEquals("# valid comment", actionLineToBeChanged.getComment()); } try { actionLineToBeChanged.setComment("# line1 \n line2"); actionLineToBeChanged.setComment("line1 \n line2"); actionLineToBeChanged.setComment("\n"); actionLineToBeChanged.setComment("# line1 \r line2"); actionLineToBeChanged.setComment("line1 \r line2"); actionLineToBeChanged.setComment("\r"); actionLineToBeChanged.setComment("# line1 \n\r line2"); actionLineToBeChanged.setComment("line1 \n\r line2"); actionLineToBeChanged.setComment("\n\r"); fail("Setting a multiline comment string should fail but doesn't"); } catch (IllegalArgumentException e) { // expected } // Try setting valid comments actionLineToBeChanged.setComment("# valid comment"); assertEquals("# valid comment", actionLineToBeChanged.getComment()); actionLineToBeChanged.setComment("# \t \t valid comment"); assertEquals("# \t \t valid comment", actionLineToBeChanged.getComment()); actionLineToBeChanged.setComment("# "); assertEquals("# ", actionLineToBeChanged.getComment()); actionLineToBeChanged.setComment(""); assertEquals("", actionLineToBeChanged.getComment()); actionLineToBeChanged.setComment(" "); assertEquals(" ", actionLineToBeChanged.getComment()); actionLineToBeChanged.setComment("\t\t"); assertEquals("\t\t", actionLineToBeChanged.getComment()); actionLineToBeChanged.setComment(null); assertEquals(null, actionLineToBeChanged.getComment()); } @Test public void testRebaseInteractiveReword() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master").call(); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); git.commit().setMessage("update file2 on side").call(); RebaseResult res = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.REWORD); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return "rewritten commit message"; } }).call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); checkFile(new File(db.getWorkTree(), "file2"), "more change"); assertEquals(Status.OK, res.getStatus()); Iterator<RevCommit> logIterator = git.log().all().call().iterator(); logIterator.next(); // skip first commit; String actualCommitMag = logIterator.next().getShortMessage(); assertEquals("rewritten commit message", actualCommitMag); } @Test public void testRebaseInteractiveEdit() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master").call(); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); git.commit().setMessage("update file2 on side").call(); RebaseResult res = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.EDIT); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return ""; // not used } }).call(); assertEquals(Status.EDIT, res.getStatus()); RevCommit toBeEditted = git.log().call().iterator().next(); assertEquals("updated file1 on master", toBeEditted.getFullMessage()); // change file and commit with new commit message writeTrashFile("file1", "edited"); git.commit().setAll(true).setAmend(true) .setMessage("edited commit message").call(); // resume rebase res = git.rebase().setOperation(Operation.CONTINUE).call(); checkFile(new File(db.getWorkTree(), "file1"), "edited"); assertEquals(Status.OK, res.getStatus()); Iterator<RevCommit> logIterator = git.log().all().call().iterator(); logIterator.next(); // skip first commit; String actualCommitMag = logIterator.next().getShortMessage(); assertEquals("edited commit message", actualCommitMag); } @Test public void testParseSquashFixupSequenceCount() { int count = RebaseCommand .parseSquashFixupSequenceCount("# This is a combination of 3 commits.\n# newline"); assertEquals(3, count); } @Test public void testRebaseInteractiveSingleSquashAndModifyMessage() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master\nnew line").call(); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); git.commit().setMessage("update file2 on master\nnew line").call(); git.rebase().setUpstream("HEAD~3") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(1).setAction(Action.SQUASH); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { final File messageSquashFile = new File(db .getDirectory(), "rebase-merge/message-squash"); final File messageFixupFile = new File(db .getDirectory(), "rebase-merge/message-fixup"); assertFalse(messageFixupFile.exists()); assertTrue(messageSquashFile.exists()); assertEquals( "# This is a combination of 2 commits.\n# The first commit's message is:\nAdd file2\nnew line\n# This is the 2nd commit message:\nupdated file1 on master\nnew line", commit); try { byte[] messageSquashBytes = IO .readFully(messageSquashFile); int end = RawParseUtils.prevLF(messageSquashBytes, messageSquashBytes.length); String messageSquashContent = RawParseUtils.decode( messageSquashBytes, 0, end + 1); assertEquals(messageSquashContent, commit); } catch (Throwable t) { fail(t.getMessage()); } return "changed"; } }).call(); try (RevWalk walk = new RevWalk(db)) { ObjectId headId = db.resolve(Constants.HEAD); RevCommit headCommit = walk.parseCommit(headId); assertEquals(headCommit.getFullMessage(), "update file2 on master\nnew line"); ObjectId head2Id = db.resolve(Constants.HEAD + "^1"); RevCommit head1Commit = walk.parseCommit(head2Id); assertEquals("changed", head1Commit.getFullMessage()); } } @Test public void testRebaseInteractiveMultipleSquash() throws Exception { // create file0 on master writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("Add file0\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file0").exists()); // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master\nnew line").call(); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); git.commit().setMessage("update file2 on master\nnew line").call(); git.rebase().setUpstream("HEAD~4") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(1).setAction(Action.SQUASH); steps.get(2).setAction(Action.SQUASH); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { final File messageSquashFile = new File(db.getDirectory(), "rebase-merge/message-squash"); final File messageFixupFile = new File(db.getDirectory(), "rebase-merge/message-fixup"); assertFalse(messageFixupFile.exists()); assertTrue(messageSquashFile.exists()); assertEquals( "# This is a combination of 3 commits.\n# The first commit's message is:\nAdd file1\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# This is the 3rd commit message:\nupdated file1 on master\nnew line", commit); try { byte[] messageSquashBytes = IO .readFully(messageSquashFile); int end = RawParseUtils.prevLF(messageSquashBytes, messageSquashBytes.length); String messageSquashContend = RawParseUtils.decode( messageSquashBytes, 0, end + 1); assertEquals(messageSquashContend, commit); } catch (Throwable t) { fail(t.getMessage()); } return "# This is a combination of 3 commits.\n# The first commit's message is:\nAdd file1\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# This is the 3rd commit message:\nupdated file1 on master\nnew line"; } }).call(); try (RevWalk walk = new RevWalk(db)) { ObjectId headId = db.resolve(Constants.HEAD); RevCommit headCommit = walk.parseCommit(headId); assertEquals(headCommit.getFullMessage(), "update file2 on master\nnew line"); ObjectId head2Id = db.resolve(Constants.HEAD + "^1"); RevCommit head1Commit = walk.parseCommit(head2Id); assertEquals( "Add file1\nnew line\nAdd file2\nnew line\nupdated file1 on master\nnew line", head1Commit.getFullMessage()); } } @Test public void testRebaseInteractiveMixedSquashAndFixup() throws Exception { // create file0 on master writeTrashFile("file0", "file0"); git.add().addFilepattern("file0").call(); git.commit().setMessage("Add file0\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file0").exists()); // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master\nnew line").call(); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); git.commit().setMessage("update file2 on master\nnew line").call(); git.rebase().setUpstream("HEAD~4") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(1).setAction(Action.FIXUP); steps.get(2).setAction(Action.SQUASH); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { final File messageSquashFile = new File(db .getDirectory(), "rebase-merge/message-squash"); final File messageFixupFile = new File(db .getDirectory(), "rebase-merge/message-fixup"); assertFalse(messageFixupFile.exists()); assertTrue(messageSquashFile.exists()); assertEquals( "# This is a combination of 3 commits.\n# The first commit's message is:\nAdd file1\nnew line\n# The 2nd commit message will be skipped:\n# Add file2\n# new line\n# This is the 3rd commit message:\nupdated file1 on master\nnew line", commit); try { byte[] messageSquashBytes = IO .readFully(messageSquashFile); int end = RawParseUtils.prevLF(messageSquashBytes, messageSquashBytes.length); String messageSquashContend = RawParseUtils.decode( messageSquashBytes, 0, end + 1); assertEquals(messageSquashContend, commit); } catch (Throwable t) { fail(t.getMessage()); } return "changed"; } }).call(); try (RevWalk walk = new RevWalk(db)) { ObjectId headId = db.resolve(Constants.HEAD); RevCommit headCommit = walk.parseCommit(headId); assertEquals(headCommit.getFullMessage(), "update file2 on master\nnew line"); ObjectId head2Id = db.resolve(Constants.HEAD + "^1"); RevCommit head1Commit = walk.parseCommit(head2Id); assertEquals("changed", head1Commit.getFullMessage()); } } @Test public void testRebaseInteractiveSingleFixup() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master\nnew line").call(); writeTrashFile("file2", "more change"); git.add().addFilepattern("file2").call(); git.commit().setMessage("update file2 on master\nnew line").call(); git.rebase().setUpstream("HEAD~3") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(1).setAction(Action.FIXUP); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { fail("No callback to modify commit message expected for single fixup"); return commit; } }).call(); try (RevWalk walk = new RevWalk(db)) { ObjectId headId = db.resolve(Constants.HEAD); RevCommit headCommit = walk.parseCommit(headId); assertEquals("update file2 on master\nnew line", headCommit.getFullMessage()); ObjectId head1Id = db.resolve(Constants.HEAD + "^1"); RevCommit head1Commit = walk.parseCommit(head1Id); assertEquals("Add file2\nnew line", head1Commit.getFullMessage()); } } @Test public void testRebaseInteractiveFixupWithBlankLines() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("updated file1 on master\n\nsome text").call(); git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(1).setAction(Action.FIXUP); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { fail("No callback to modify commit message expected for single fixup"); return commit; } }).call(); try (RevWalk walk = new RevWalk(db)) { ObjectId headId = db.resolve(Constants.HEAD); RevCommit headCommit = walk.parseCommit(headId); assertEquals("Add file2", headCommit.getFullMessage()); } } @Test(expected = InvalidRebaseStepException.class) public void testRebaseInteractiveFixupFirstCommitShouldFail() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); git.rebase().setUpstream("HEAD~1") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.FIXUP); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return commit; } }).call(); } @Test(expected = InvalidRebaseStepException.class) public void testRebaseInteractiveSquashFirstCommitShouldFail() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); git.rebase().setUpstream("HEAD~1") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.SQUASH); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return commit; } }).call(); } @Test public void testRebaseEndsIfLastStepIsEdit() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Add file2\nnew line").call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); git.rebase().setUpstream("HEAD~1") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.EDIT); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return commit; } }).call(); git.commit().setAmend(true) .setMessage("Add file2\nnew line\nanother line").call(); RebaseResult result = git.rebase().setOperation(Operation.CONTINUE) .call(); assertEquals(Status.OK, result.getStatus()); } @Test public void testRebaseShouldStopForEditInCaseOfConflict() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); //change file1 writeTrashFile(FILE1, FILE1 + "a"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); //change file1 writeTrashFile(FILE1, FILE1 + "b"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); RebaseResult result = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { steps.remove(0); try { steps.get(0).setAction(Action.EDIT); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return commit; } }).call(); assertEquals(Status.STOPPED, result.getStatus()); git.add().addFilepattern(FILE1).call(); result = git.rebase().setOperation(Operation.CONTINUE).call(); assertEquals(Status.EDIT, result.getStatus()); } @Test public void testRebaseShouldStopForRewordInCaseOfConflict() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change file1 writeTrashFile(FILE1, FILE1 + "a"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); // change file1 writeTrashFile(FILE1, FILE1 + "b"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); RebaseResult result = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { steps.remove(0); try { steps.get(0).setAction(Action.REWORD); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return "rewritten commit message"; } }).call(); assertEquals(Status.STOPPED, result.getStatus()); git.add().addFilepattern(FILE1).call(); result = git.rebase().runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { steps.remove(0); try { steps.get(0).setAction(Action.REWORD); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return "rewritten commit message"; } }).setOperation(Operation.CONTINUE).call(); assertEquals(Status.OK, result.getStatus()); Iterator<RevCommit> logIterator = git.log().all().call().iterator(); String actualCommitMag = logIterator.next().getShortMessage(); assertEquals("rewritten commit message", actualCommitMag); } @Test public void testRebaseShouldSquashInCaseOfConflict() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1\nnew line").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change file2 writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Change file2").call(); // change file1 writeTrashFile(FILE1, FILE1 + "a"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); // change file1 writeTrashFile(FILE1, FILE1 + "b"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); RebaseResult result = git.rebase().setUpstream("HEAD~3") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.PICK); steps.remove(1); steps.get(1).setAction(Action.SQUASH); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return "squashed message"; } }).call(); assertEquals(Status.STOPPED, result.getStatus()); git.add().addFilepattern(FILE1).call(); result = git.rebase().runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.PICK); steps.remove(1); steps.get(1).setAction(Action.SQUASH); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return "squashed message"; } }).setOperation(Operation.CONTINUE).call(); assertEquals(Status.OK, result.getStatus()); Iterator<RevCommit> logIterator = git.log().all().call().iterator(); String actualCommitMag = logIterator.next().getShortMessage(); assertEquals("squashed message", actualCommitMag); } @Test public void testRebaseShouldFixupInCaseOfConflict() throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1").call(); assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change file2 writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); git.commit().setMessage("Change file2").call(); // change file1 writeTrashFile(FILE1, FILE1 + "a"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Change file1").call(); // change file1, add file3 writeTrashFile(FILE1, FILE1 + "b"); writeTrashFile("file3", "file3"); git.add().addFilepattern(FILE1).call(); git.add().addFilepattern("file3").call(); git.commit().setMessage("Change file1, add file3").call(); RebaseResult result = git.rebase().setUpstream("HEAD~3") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.PICK); steps.remove(1); steps.get(1).setAction(Action.FIXUP); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return commit; } }).call(); assertEquals(Status.STOPPED, result.getStatus()); git.add().addFilepattern(FILE1).call(); result = git.rebase().runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.PICK); steps.remove(1); steps.get(1).setAction(Action.FIXUP); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return "commit"; } }).setOperation(Operation.CONTINUE).call(); assertEquals(Status.OK, result.getStatus()); Iterator<RevCommit> logIterator = git.log().all().call().iterator(); String actualCommitMsg = logIterator.next().getShortMessage(); assertEquals("Change file2", actualCommitMsg); actualCommitMsg = logIterator.next().getShortMessage(); assertEquals("Add file1", actualCommitMsg); assertTrue(new File(db.getWorkTree(), "file3").exists()); } @Test public void testInteractiveRebaseWithModificationShouldNotDeleteDataOnAbort() throws Exception { // create file0 + file1, add and commit writeTrashFile("file0", "file0"); writeTrashFile(FILE1, "file1"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); git.commit().setMessage("commit1").call(); // modify file1, add and commit writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit2").call(); // modify file1, add and commit writeTrashFile(FILE1, "modified file1 a second time"); git.add().addFilepattern(FILE1).call(); git.commit().setMessage("commit3").call(); // modify file0, but do not commit writeTrashFile("file0", "modified file0 in index"); git.add().addFilepattern("file0").addFilepattern(FILE1).call(); // do not commit writeTrashFile("file0", "modified file0"); // start rebase RebaseResult result = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { public void prepareSteps(List<RebaseTodoLine> steps) { try { steps.get(0).setAction(Action.EDIT); steps.get(1).setAction(Action.PICK); } catch (IllegalTodoFileModification e) { fail("unexpected exception: " + e); } } public String modifyCommitMessage(String commit) { return commit; } }).call(); // the following condition was true before commit 83b6ab233: // jgit started the rebase and deleted the change on abort // This test should verify that content was deleted if (result.getStatus() == Status.EDIT) git.rebase().setOperation(Operation.ABORT).call(); checkFile(new File(db.getWorkTree(), "file0"), "modified file0"); checkFile(new File(db.getWorkTree(), "file1"), "modified file1 a second time"); assertEquals("[file0, mode:100644, content:modified file0 in index]" + "[file1, mode:100644, content:modified file1 a second time]", indexState(CONTENT)); } private File getTodoFile() { File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO); return todoFile; } }