Merge changes I40f2311c,I3c419094
* changes: Add additional RebaseResult for editing commits Add Squash/Fixup support for rebase interactive in RebaseCommand
This commit is contained in:
commit
34fbd814d4
|
@ -42,9 +42,9 @@
|
||||||
*/
|
*/
|
||||||
package org.eclipse.jgit.api;
|
package org.eclipse.jgit.api;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.not;
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
@ -63,6 +63,7 @@
|
||||||
import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler;
|
import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler;
|
||||||
import org.eclipse.jgit.api.RebaseCommand.Operation;
|
import org.eclipse.jgit.api.RebaseCommand.Operation;
|
||||||
import org.eclipse.jgit.api.RebaseResult.Status;
|
import org.eclipse.jgit.api.RebaseResult.Status;
|
||||||
|
import org.eclipse.jgit.api.errors.InvalidRebaseStepException;
|
||||||
import org.eclipse.jgit.api.errors.JGitInternalException;
|
import org.eclipse.jgit.api.errors.JGitInternalException;
|
||||||
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
||||||
import org.eclipse.jgit.api.errors.UnmergedPathsException;
|
import org.eclipse.jgit.api.errors.UnmergedPathsException;
|
||||||
|
@ -83,6 +84,8 @@
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
import org.eclipse.jgit.util.FileUtils;
|
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.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -1938,7 +1941,7 @@ public String modifyCommitMessage(String commit) {
|
||||||
return ""; // not used
|
return ""; // not used
|
||||||
}
|
}
|
||||||
}).call();
|
}).call();
|
||||||
assertEquals(Status.STOPPED, res.getStatus());
|
assertEquals(Status.EDIT, res.getStatus());
|
||||||
RevCommit toBeEditted = git.log().call().iterator().next();
|
RevCommit toBeEditted = git.log().call().iterator().next();
|
||||||
assertEquals("updated file1 on master", toBeEditted.getFullMessage());
|
assertEquals("updated file1 on master", toBeEditted.getFullMessage());
|
||||||
|
|
||||||
|
@ -1957,6 +1960,340 @@ public String modifyCommitMessage(String commit) {
|
||||||
assertEquals("edited commit message", actualCommitMag);
|
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) {
|
||||||
|
steps.get(1).setAction(Action.SQUASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
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# This is the 2nd commit message:\nupdated file1 on master\nnew line\n# The first commit's message is:\nAdd file2\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();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
steps.get(1).setAction(Action.SQUASH);
|
||||||
|
steps.get(2).setAction(Action.SQUASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
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# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# The first commit's message is:\nAdd file1\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# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# This is the 2nd commit message:\nAdd file2\nnew line\n# The first commit's message is:\nAdd file1\nnew line";
|
||||||
|
}
|
||||||
|
}).call();
|
||||||
|
|
||||||
|
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(
|
||||||
|
"updated file1 on master\nnew line\nAdd file2\nnew line\nAdd file1\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) {
|
||||||
|
steps.get(1).setAction(Action.FIXUP);
|
||||||
|
steps.get(2).setAction(Action.SQUASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
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# This is the 3rd commit message:\nupdated file1 on master\nnew line\n# The 2nd commit message will be skipped:\n# Add file2\n# new line\n# The first commit's message is:\nAdd file1\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();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
steps.get(1).setAction(Action.FIXUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String modifyCommitMessage(String commit) {
|
||||||
|
fail("No callback to modify commit message expected for single fixup");
|
||||||
|
return commit;
|
||||||
|
}
|
||||||
|
}).call();
|
||||||
|
|
||||||
|
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(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) {
|
||||||
|
steps.get(0).setAction(Action.FIXUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
steps.get(0).setAction(Action.SQUASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String modifyCommitMessage(String commit) {
|
||||||
|
return commit;
|
||||||
|
}
|
||||||
|
}).call();
|
||||||
|
}
|
||||||
|
|
||||||
private File getTodoFile() {
|
private File getTodoFile() {
|
||||||
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
|
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
|
||||||
return todoFile;
|
return todoFile;
|
||||||
|
|
|
@ -78,6 +78,7 @@ cannotReadObject=Cannot read object
|
||||||
cannotReadTree=Cannot read tree {0}
|
cannotReadTree=Cannot read tree {0}
|
||||||
cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD
|
cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD
|
||||||
cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating.
|
cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating.
|
||||||
|
cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit.
|
||||||
cannotStoreObjects=cannot store objects
|
cannotStoreObjects=cannot store objects
|
||||||
cannotUnloadAModifiedTree=Cannot unload a modified tree.
|
cannotUnloadAModifiedTree=Cannot unload a modified tree.
|
||||||
cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index.
|
cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index.
|
||||||
|
|
|
@ -55,10 +55,14 @@
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.RebaseResult.Status;
|
import org.eclipse.jgit.api.RebaseResult.Status;
|
||||||
|
import org.eclipse.jgit.api.ResetCommand.ResetType;
|
||||||
import org.eclipse.jgit.api.errors.CheckoutConflictException;
|
import org.eclipse.jgit.api.errors.CheckoutConflictException;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.api.errors.InvalidRebaseStepException;
|
||||||
import org.eclipse.jgit.api.errors.InvalidRefNameException;
|
import org.eclipse.jgit.api.errors.InvalidRefNameException;
|
||||||
import org.eclipse.jgit.api.errors.JGitInternalException;
|
import org.eclipse.jgit.api.errors.JGitInternalException;
|
||||||
import org.eclipse.jgit.api.errors.NoHeadException;
|
import org.eclipse.jgit.api.errors.NoHeadException;
|
||||||
|
@ -147,6 +151,10 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
|
||||||
|
|
||||||
private static final String AMEND = "amend"; //$NON-NLS-1$
|
private static final String AMEND = "amend"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private static final String MESSAGE_FIXUP = "message-fixup"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The available operations
|
* The available operations
|
||||||
*/
|
*/
|
||||||
|
@ -281,7 +289,9 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
||||||
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
|
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
|
||||||
steps, false);
|
steps, false);
|
||||||
}
|
}
|
||||||
for (RebaseTodoLine step : steps) {
|
checkSteps(steps);
|
||||||
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|
RebaseTodoLine step = steps.get(i);
|
||||||
popSteps(1);
|
popSteps(1);
|
||||||
if (Action.COMMENT.equals(step.getAction()))
|
if (Action.COMMENT.equals(step.getAction()))
|
||||||
continue;
|
continue;
|
||||||
|
@ -292,7 +302,7 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
||||||
RevCommit commitToPick = walk
|
RevCommit commitToPick = walk
|
||||||
.parseCommit(ids.iterator().next());
|
.parseCommit(ids.iterator().next());
|
||||||
if (monitor.isCancelled())
|
if (monitor.isCancelled())
|
||||||
return new RebaseResult(commitToPick);
|
return new RebaseResult(commitToPick, Status.STOPPED);
|
||||||
try {
|
try {
|
||||||
monitor.beginTask(MessageFormat.format(
|
monitor.beginTask(MessageFormat.format(
|
||||||
JGitText.get().applyingCommit,
|
JGitText.get().applyingCommit,
|
||||||
|
@ -318,13 +328,14 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
||||||
return abort(new RebaseResult(
|
return abort(new RebaseResult(
|
||||||
cherryPickResult.getFailingPaths()));
|
cherryPickResult.getFailingPaths()));
|
||||||
else
|
else
|
||||||
return stop(commitToPick);
|
return stop(commitToPick, Status.STOPPED);
|
||||||
case CONFLICTING:
|
case CONFLICTING:
|
||||||
return stop(commitToPick);
|
return stop(commitToPick, Status.STOPPED);
|
||||||
case OK:
|
case OK:
|
||||||
newHead = cherryPickResult.getNewHead();
|
newHead = cherryPickResult.getNewHead();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
boolean isSquash = false;
|
||||||
switch (step.getAction()) {
|
switch (step.getAction()) {
|
||||||
case PICK:
|
case PICK:
|
||||||
continue; // continue rebase process on pick command
|
continue; // continue rebase process on pick command
|
||||||
|
@ -337,9 +348,23 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
||||||
continue;
|
continue;
|
||||||
case EDIT:
|
case EDIT:
|
||||||
rebaseState.createFile(AMEND, commitToPick.name());
|
rebaseState.createFile(AMEND, commitToPick.name());
|
||||||
return stop(commitToPick);
|
return stop(commitToPick, Status.EDIT);
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
break;
|
break;
|
||||||
|
case SQUASH:
|
||||||
|
isSquash = true;
|
||||||
|
//$FALL-THROUGH$
|
||||||
|
case FIXUP:
|
||||||
|
resetSoftToParent();
|
||||||
|
RebaseTodoLine nextStep = (i >= steps.size() - 1 ? null
|
||||||
|
: steps.get(i + 1));
|
||||||
|
File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP);
|
||||||
|
File messageSquashFile = rebaseState
|
||||||
|
.getFile(MESSAGE_SQUASH);
|
||||||
|
if (isSquash && messageFixupFile.exists())
|
||||||
|
messageFixupFile.delete();
|
||||||
|
newHead = doSquashFixup(isSquash, commitToPick,
|
||||||
|
nextStep, messageFixupFile, messageSquashFile);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
monitor.endTask();
|
monitor.endTask();
|
||||||
|
@ -361,6 +386,175 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkSteps(List<RebaseTodoLine> steps)
|
||||||
|
throws InvalidRebaseStepException, IOException {
|
||||||
|
if (steps.isEmpty())
|
||||||
|
return;
|
||||||
|
if (RebaseTodoLine.Action.SQUASH.equals(steps.get(0).getAction())
|
||||||
|
|| RebaseTodoLine.Action.FIXUP.equals(steps.get(0).getAction())) {
|
||||||
|
if (!rebaseState.getFile(DONE).exists()
|
||||||
|
|| rebaseState.readFile(DONE).trim().length() == 0) {
|
||||||
|
throw new InvalidRebaseStepException(MessageFormat.format(
|
||||||
|
JGitText.get().cannotSquashFixupWithoutPreviousCommit,
|
||||||
|
steps.get(0).getAction().name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick,
|
||||||
|
RebaseTodoLine nextStep, File messageFixup, File messageSquash)
|
||||||
|
throws IOException, GitAPIException {
|
||||||
|
|
||||||
|
if (!messageSquash.exists()) {
|
||||||
|
// init squash/fixup sequence
|
||||||
|
ObjectId headId = repo.resolve(Constants.HEAD);
|
||||||
|
RevCommit previousCommit = walk.parseCommit(headId);
|
||||||
|
|
||||||
|
initializeSquashFixupFile(MESSAGE_SQUASH,
|
||||||
|
previousCommit.getFullMessage());
|
||||||
|
if (!isSquash)
|
||||||
|
initializeSquashFixupFile(MESSAGE_FIXUP,
|
||||||
|
previousCommit.getFullMessage());
|
||||||
|
}
|
||||||
|
String currSquashMessage = rebaseState
|
||||||
|
.readFile(MESSAGE_SQUASH);
|
||||||
|
|
||||||
|
int count = parseSquashFixupSequenceCount(currSquashMessage) + 1;
|
||||||
|
|
||||||
|
String content = composeSquashMessage(isSquash,
|
||||||
|
commitToPick, currSquashMessage, count);
|
||||||
|
rebaseState.createFile(MESSAGE_SQUASH, content);
|
||||||
|
if (messageFixup.exists())
|
||||||
|
rebaseState.createFile(MESSAGE_FIXUP, content);
|
||||||
|
|
||||||
|
return squashIntoPrevious(
|
||||||
|
!messageFixup.exists(),
|
||||||
|
nextStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetSoftToParent() throws IOException,
|
||||||
|
GitAPIException, CheckoutConflictException {
|
||||||
|
Ref orig_head = repo.getRef(Constants.ORIG_HEAD);
|
||||||
|
ObjectId orig_headId = orig_head.getObjectId();
|
||||||
|
try {
|
||||||
|
// we have already commited the cherry-picked commit.
|
||||||
|
// what we need is to have changes introduced by this
|
||||||
|
// commit to be on the index
|
||||||
|
// resetting is a workaround
|
||||||
|
Git.wrap(repo).reset().setMode(ResetType.SOFT)
|
||||||
|
.setRef("HEAD~1").call(); //$NON-NLS-1$
|
||||||
|
} finally {
|
||||||
|
// set ORIG_HEAD back to where we started because soft
|
||||||
|
// reset moved it
|
||||||
|
repo.writeOrigHead(orig_headId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RevCommit squashIntoPrevious(boolean sequenceContainsSquash,
|
||||||
|
RebaseTodoLine nextStep)
|
||||||
|
throws IOException, GitAPIException {
|
||||||
|
RevCommit newHead;
|
||||||
|
String commitMessage = rebaseState
|
||||||
|
.readFile(MESSAGE_SQUASH);
|
||||||
|
|
||||||
|
if (nextStep == null
|
||||||
|
|| ((nextStep.getAction() != Action.FIXUP) && (nextStep
|
||||||
|
.getAction() != Action.SQUASH))) {
|
||||||
|
// this is the last step in this sequence
|
||||||
|
if (sequenceContainsSquash) {
|
||||||
|
commitMessage = interactiveHandler
|
||||||
|
.modifyCommitMessage(commitMessage);
|
||||||
|
}
|
||||||
|
newHead = new Git(repo).commit()
|
||||||
|
.setMessage(stripCommentLines(commitMessage))
|
||||||
|
.setAmend(true).call();
|
||||||
|
rebaseState.getFile(MESSAGE_SQUASH).delete();
|
||||||
|
rebaseState.getFile(MESSAGE_FIXUP).delete();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Next step is either Squash or Fixup
|
||||||
|
newHead = new Git(repo).commit()
|
||||||
|
.setMessage(commitMessage).setAmend(true)
|
||||||
|
.call();
|
||||||
|
}
|
||||||
|
return newHead;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripCommentLines(String commitMessage) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (String line : commitMessage.split("\n")) { //$NON-NLS-1$
|
||||||
|
if (!line.trim().startsWith("#")) //$NON-NLS-1$
|
||||||
|
result.append(line).append("\n"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
if (!commitMessage.endsWith("\n")) //$NON-NLS-1$
|
||||||
|
result.deleteCharAt(result.length() - 1);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
private static String composeSquashMessage(boolean isSquash,
|
||||||
|
RevCommit commitToPick, String currSquashMessage, int count) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String ordinal = getOrdinal(count);
|
||||||
|
sb.setLength(0);
|
||||||
|
sb.append("# This is a combination of ").append(count)
|
||||||
|
.append(" commits.\n");
|
||||||
|
if (isSquash) {
|
||||||
|
sb.append("# This is the ").append(count).append(ordinal)
|
||||||
|
.append(" commit message:\n");
|
||||||
|
sb.append(commitToPick.getFullMessage());
|
||||||
|
} else {
|
||||||
|
sb.append("# The ").append(count).append(ordinal)
|
||||||
|
.append(" commit message will be skipped:\n# ");
|
||||||
|
sb.append(commitToPick.getFullMessage().replaceAll("([\n\r]+)",
|
||||||
|
"$1# "));
|
||||||
|
}
|
||||||
|
// Add the previous message without header (i.e first line)
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append(currSquashMessage.substring(currSquashMessage.indexOf("\n") + 1));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getOrdinal(int count) {
|
||||||
|
switch (count % 10) {
|
||||||
|
case 1:
|
||||||
|
return "st"; //$NON-NLS-1$
|
||||||
|
case 2:
|
||||||
|
return "nd"; //$NON-NLS-1$
|
||||||
|
case 3:
|
||||||
|
return "rd"; //$NON-NLS-1$
|
||||||
|
default:
|
||||||
|
return "th"; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the count from squashed commit messages
|
||||||
|
*
|
||||||
|
* @param currSquashMessage
|
||||||
|
* the squashed commit message to be parsed
|
||||||
|
* @return the count of squashed messages in the given string
|
||||||
|
*/
|
||||||
|
static int parseSquashFixupSequenceCount(String currSquashMessage) {
|
||||||
|
String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$
|
||||||
|
String firstLine = currSquashMessage.substring(0,
|
||||||
|
currSquashMessage.indexOf("\n")); //$NON-NLS-1$
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(firstLine);
|
||||||
|
if (!matcher.find())
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
return Integer.parseInt(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeSquashFixupFile(String messageFile,
|
||||||
|
String fullMessage) throws IOException {
|
||||||
|
rebaseState
|
||||||
|
.createFile(
|
||||||
|
messageFile,
|
||||||
|
"# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$);
|
||||||
|
}
|
||||||
|
|
||||||
private String getOurCommitName() {
|
private String getOurCommitName() {
|
||||||
// If onto is different from upstream, this should say "onto", but
|
// If onto is different from upstream, this should say "onto", but
|
||||||
// RebaseCommand doesn't support a different "onto" at the moment.
|
// RebaseCommand doesn't support a different "onto" at the moment.
|
||||||
|
@ -479,7 +673,8 @@ private PersonIdent parseAuthor() throws IOException {
|
||||||
return parseAuthor(raw);
|
return parseAuthor(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RebaseResult stop(RevCommit commitToPick) throws IOException {
|
private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status)
|
||||||
|
throws IOException {
|
||||||
PersonIdent author = commitToPick.getAuthorIdent();
|
PersonIdent author = commitToPick.getAuthorIdent();
|
||||||
String authorScript = toAuthorScript(author);
|
String authorScript = toAuthorScript(author);
|
||||||
rebaseState.createFile(AUTHOR_SCRIPT, authorScript);
|
rebaseState.createFile(AUTHOR_SCRIPT, authorScript);
|
||||||
|
@ -497,7 +692,7 @@ private RebaseResult stop(RevCommit commitToPick) throws IOException {
|
||||||
// Remove cherry pick state file created by CherryPickCommand, it's not
|
// Remove cherry pick state file created by CherryPickCommand, it's not
|
||||||
// needed for rebase
|
// needed for rebase
|
||||||
repo.writeCherryPickHead(null);
|
repo.writeCherryPickHead(null);
|
||||||
return new RebaseResult(commitToPick);
|
return new RebaseResult(commitToPick, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
String toAuthorScript(PersonIdent author) {
|
String toAuthorScript(PersonIdent author) {
|
||||||
|
|
|
@ -84,6 +84,15 @@ public boolean isSuccessful() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Stopped for editing in the context of an interactive rebase
|
||||||
|
*/
|
||||||
|
EDIT {
|
||||||
|
@Override
|
||||||
|
public boolean isSuccessful() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Failed; the original HEAD was restored
|
* Failed; the original HEAD was restored
|
||||||
*/
|
*/
|
||||||
|
@ -183,9 +192,10 @@ private RebaseResult(Status status) {
|
||||||
*
|
*
|
||||||
* @param commit
|
* @param commit
|
||||||
* current commit
|
* current commit
|
||||||
|
* @param status
|
||||||
*/
|
*/
|
||||||
RebaseResult(RevCommit commit) {
|
RebaseResult(RevCommit commit, RebaseResult.Status status) {
|
||||||
status = Status.STOPPED;
|
this.status = status;
|
||||||
currentCommit = commit;
|
currentCommit = commit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013, Stefan Lay <stefan.lay@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.errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown if a rebase step is invalid. E.g., a rebase must not start
|
||||||
|
* with squash or fixup.
|
||||||
|
*/
|
||||||
|
public class InvalidRebaseStepException extends GitAPIException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
/**
|
||||||
|
* @param msg
|
||||||
|
*/
|
||||||
|
public InvalidRebaseStepException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param msg
|
||||||
|
* @param cause
|
||||||
|
*/
|
||||||
|
public InvalidRebaseStepException(String msg, Throwable cause) {
|
||||||
|
super(msg, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -140,6 +140,7 @@ public static JGitText get() {
|
||||||
/***/ public String cannotReadTree;
|
/***/ public String cannotReadTree;
|
||||||
/***/ public String cannotRebaseWithoutCurrentHead;
|
/***/ public String cannotRebaseWithoutCurrentHead;
|
||||||
/***/ public String cannotResolveLocalTrackingRefForUpdating;
|
/***/ public String cannotResolveLocalTrackingRefForUpdating;
|
||||||
|
/***/ public String cannotSquashFixupWithoutPreviousCommit;
|
||||||
/***/ public String cannotStoreObjects;
|
/***/ public String cannotStoreObjects;
|
||||||
/***/ public String cannotUnloadAModifiedTree;
|
/***/ public String cannotUnloadAModifiedTree;
|
||||||
/***/ public String cannotWorkWithOtherStagesThanZeroRightNow;
|
/***/ public String cannotWorkWithOtherStagesThanZeroRightNow;
|
||||||
|
|
|
@ -66,7 +66,11 @@ public static enum Action {
|
||||||
/** Use commit, but stop for amending */
|
/** Use commit, but stop for amending */
|
||||||
EDIT("edit", "e"),
|
EDIT("edit", "e"),
|
||||||
|
|
||||||
// TODO: add SQUASH, FIXUP, etc.
|
/** Use commit, but meld into previous commit */
|
||||||
|
SQUASH("squash", "s"),
|
||||||
|
|
||||||
|
/** like "squash", but discard this commit's log message */
|
||||||
|
FIXUP("fixup", "f"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A comment in the file. Also blank lines (or lines containing only
|
* A comment in the file. Also blank lines (or lines containing only
|
||||||
|
|
Loading…
Reference in New Issue