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;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
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;
|
||||
|
@ -63,6 +63,7 @@
|
|||
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.JGitInternalException;
|
||||
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
||||
import org.eclipse.jgit.api.errors.UnmergedPathsException;
|
||||
|
@ -83,6 +84,8 @@
|
|||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
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;
|
||||
|
||||
|
@ -1938,7 +1941,7 @@ public String modifyCommitMessage(String commit) {
|
|||
return ""; // not used
|
||||
}
|
||||
}).call();
|
||||
assertEquals(Status.STOPPED, res.getStatus());
|
||||
assertEquals(Status.EDIT, res.getStatus());
|
||||
RevCommit toBeEditted = git.log().call().iterator().next();
|
||||
assertEquals("updated file1 on master", toBeEditted.getFullMessage());
|
||||
|
||||
|
@ -1957,6 +1960,340 @@ public String modifyCommitMessage(String commit) {
|
|||
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() {
|
||||
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
|
||||
return todoFile;
|
||||
|
|
|
@ -78,6 +78,7 @@ cannotReadObject=Cannot read object
|
|||
cannotReadTree=Cannot read tree {0}
|
||||
cannotRebaseWithoutCurrentHead=Can not rebase without a current HEAD
|
||||
cannotResolveLocalTrackingRefForUpdating=Cannot resolve local tracking ref {0} for updating.
|
||||
cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit.
|
||||
cannotStoreObjects=cannot store objects
|
||||
cannotUnloadAModifiedTree=Cannot unload a modified tree.
|
||||
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.List;
|
||||
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.ResetCommand.ResetType;
|
||||
import org.eclipse.jgit.api.errors.CheckoutConflictException;
|
||||
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.JGitInternalException;
|
||||
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 MESSAGE_FIXUP = "message-fixup"; //$NON-NLS-1$
|
||||
|
||||
private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* The available operations
|
||||
*/
|
||||
|
@ -281,7 +289,9 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
|||
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
|
||||
steps, false);
|
||||
}
|
||||
for (RebaseTodoLine step : steps) {
|
||||
checkSteps(steps);
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
RebaseTodoLine step = steps.get(i);
|
||||
popSteps(1);
|
||||
if (Action.COMMENT.equals(step.getAction()))
|
||||
continue;
|
||||
|
@ -292,7 +302,7 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
|||
RevCommit commitToPick = walk
|
||||
.parseCommit(ids.iterator().next());
|
||||
if (monitor.isCancelled())
|
||||
return new RebaseResult(commitToPick);
|
||||
return new RebaseResult(commitToPick, Status.STOPPED);
|
||||
try {
|
||||
monitor.beginTask(MessageFormat.format(
|
||||
JGitText.get().applyingCommit,
|
||||
|
@ -318,13 +328,14 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
|||
return abort(new RebaseResult(
|
||||
cherryPickResult.getFailingPaths()));
|
||||
else
|
||||
return stop(commitToPick);
|
||||
return stop(commitToPick, Status.STOPPED);
|
||||
case CONFLICTING:
|
||||
return stop(commitToPick);
|
||||
return stop(commitToPick, Status.STOPPED);
|
||||
case OK:
|
||||
newHead = cherryPickResult.getNewHead();
|
||||
}
|
||||
}
|
||||
boolean isSquash = false;
|
||||
switch (step.getAction()) {
|
||||
case PICK:
|
||||
continue; // continue rebase process on pick command
|
||||
|
@ -337,9 +348,23 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
|
|||
continue;
|
||||
case EDIT:
|
||||
rebaseState.createFile(AMEND, commitToPick.name());
|
||||
return stop(commitToPick);
|
||||
return stop(commitToPick, Status.EDIT);
|
||||
case COMMENT:
|
||||
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 {
|
||||
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() {
|
||||
// If onto is different from upstream, this should say "onto", but
|
||||
// RebaseCommand doesn't support a different "onto" at the moment.
|
||||
|
@ -479,7 +673,8 @@ private PersonIdent parseAuthor() throws IOException {
|
|||
return parseAuthor(raw);
|
||||
}
|
||||
|
||||
private RebaseResult stop(RevCommit commitToPick) throws IOException {
|
||||
private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status)
|
||||
throws IOException {
|
||||
PersonIdent author = commitToPick.getAuthorIdent();
|
||||
String authorScript = toAuthorScript(author);
|
||||
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
|
||||
// needed for rebase
|
||||
repo.writeCherryPickHead(null);
|
||||
return new RebaseResult(commitToPick);
|
||||
return new RebaseResult(commitToPick, status);
|
||||
}
|
||||
|
||||
String toAuthorScript(PersonIdent author) {
|
||||
|
|
|
@ -84,6 +84,15 @@ public boolean isSuccessful() {
|
|||
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
|
||||
*/
|
||||
|
@ -183,9 +192,10 @@ private RebaseResult(Status status) {
|
|||
*
|
||||
* @param commit
|
||||
* current commit
|
||||
* @param status
|
||||
*/
|
||||
RebaseResult(RevCommit commit) {
|
||||
status = Status.STOPPED;
|
||||
RebaseResult(RevCommit commit, RebaseResult.Status status) {
|
||||
this.status = status;
|
||||
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 cannotRebaseWithoutCurrentHead;
|
||||
/***/ public String cannotResolveLocalTrackingRefForUpdating;
|
||||
/***/ public String cannotSquashFixupWithoutPreviousCommit;
|
||||
/***/ public String cannotStoreObjects;
|
||||
/***/ public String cannotUnloadAModifiedTree;
|
||||
/***/ public String cannotWorkWithOtherStagesThanZeroRightNow;
|
||||
|
|
|
@ -66,7 +66,11 @@ public static enum Action {
|
|||
/** Use commit, but stop for amending */
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue