From 6bca46e1683a07f18f00f6ad552eab79ab50bb88 Mon Sep 17 00:00:00 2001 From: Mathias Kinzler Date: Thu, 9 Dec 2010 16:10:21 +0100 Subject: [PATCH] Implement rebase --continue and --skip For --continue, the Rebase command asserts that there are no unmerged paths in the current repository. Then it checks if a commit is needed. If yes, the commit message and author are taken from the author_script and message files, respectively, and a commit is performed before the next step is applied. For --skip, the workspace is reset to the current HEAD before applying the next step. Includes some tests and a refactoring that extracts Strings in the code into constants. Change-Id: I72d9968535727046e737ec20e23239fe79976179 Signed-off-by: Mathias Kinzler Signed-off-by: Christian Halstrick --- .../eclipse/jgit/api/RebaseCommandTest.java | 547 +++++++++++++++--- .../org/eclipse/jgit/JGitText.properties | 2 + .../src/org/eclipse/jgit/JGitText.java | 2 + .../org/eclipse/jgit/api/RebaseCommand.java | 266 +++++++-- .../api/errors/UnmergedPathsException.java | 54 ++ 5 files changed, 728 insertions(+), 143 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java index bddb9ed6e..35b05e719 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java @@ -51,10 +51,12 @@ import java.io.InputStreamReader; import org.eclipse.jgit.api.RebaseCommand.Operation; import org.eclipse.jgit.api.RebaseResult.Status; 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.dircache.DirCacheCheckout; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.RepositoryTestCase; @@ -62,6 +64,16 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; public class RebaseCommandTest extends RepositoryTestCase { + private static final String FILE1 = "file1"; + + protected Git git; + + @Override + protected void setUp() throws Exception { + super.setUp(); + this.git = new Git(db); + } + private void createBranch(ObjectId objectId, String branchName) throws IOException { RefUpdate updateRef = db.updateRef(branchName); @@ -88,8 +100,8 @@ public class RebaseCommandTest extends RepositoryTestCase { IOException { RevWalk walk = new RevWalk(db); RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD)); - DirCacheCheckout dco = new DirCacheCheckout(db, head.getTree(), - db.lockDirCache(), commit.getTree()); + DirCacheCheckout dco = new DirCacheCheckout(db, head.getTree(), db + .lockDirCache(), commit.getTree()); dco.setFailOnConflict(true); dco.checkout(); walk.release(); @@ -100,14 +112,12 @@ public class RebaseCommandTest extends RepositoryTestCase { } public void testFastForwardWithNewFile() throws Exception { - Git git = new Git(db); - // create file1 on master - writeTrashFile("file1", "file1"); - git.add().addFilepattern("file1").call(); + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); RevCommit first = git.commit().setMessage("Add file1").call(); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), FILE1).exists()); // create a topic branch createBranch(first, "refs/heads/topic"); // create file2 on master @@ -124,32 +134,27 @@ public class RebaseCommandTest extends RepositoryTestCase { } public void testUpToDate() throws Exception { - Git git = new Git(db); - // create file1 on master - writeTrashFile("file1", "file1"); - git.add().addFilepattern("file1").call(); + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); RevCommit first = git.commit().setMessage("Add file1").call(); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), FILE1).exists()); RebaseResult res = git.rebase().setUpstream(first).call(); assertEquals(Status.UP_TO_DATE, res.getStatus()); } public void testUnknownUpstream() throws Exception { - Git git = new Git(db); - // create file1 on master - writeTrashFile("file1", "file1"); - git.add().addFilepattern("file1").call(); + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); git.commit().setMessage("Add file1").call(); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), FILE1).exists()); try { - git.rebase().setUpstream("refs/heads/xyz") - .call(); + git.rebase().setUpstream("refs/heads/xyz").call(); fail("expected exception was not thrown"); } catch (RefNotFoundException e) { // expected exception @@ -157,17 +162,15 @@ public class RebaseCommandTest extends RepositoryTestCase { } public void testConflictFreeWithSingleFile() throws Exception { - Git git = new Git(db); - // create file1 on master - File theFile = writeTrashFile("file1", "1\n2\n3\n"); - git.add().addFilepattern("file1").call(); + 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()); + assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change first line in master and commit - writeTrashFile("file1", "1master\n2\n3\n"); + writeTrashFile(FILE1, "1master\n2\n3\n"); checkFile(theFile, "1master\n2\n3\n"); - git.add().addFilepattern("file1").call(); + git.add().addFilepattern(FILE1).call(); RevCommit lastMasterChange = git.commit().setMessage( "change file1 in master").call(); @@ -177,10 +180,10 @@ public class RebaseCommandTest extends RepositoryTestCase { // we have the old content again checkFile(theFile, "1\n2\n3\n"); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + 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(); + writeTrashFile(FILE1, "1\n2\n3\ntopic\n"); + git.add().addFilepattern(FILE1).call(); git.commit().setMessage("change file1 in topic").call(); RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); @@ -193,19 +196,17 @@ public class RebaseCommandTest extends RepositoryTestCase { } public void testDetachedHead() throws Exception { - Git git = new Git(db); - // create file1 on master - File theFile = writeTrashFile("file1", "1\n2\n3\n"); - git.add().addFilepattern("file1").call(); + 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()); + assertTrue(new File(db.getWorkTree(), FILE1).exists()); // change first line in master and commit - writeTrashFile("file1", "1master\n2\n3\n"); + 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(); + 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"); @@ -213,10 +214,10 @@ public class RebaseCommandTest extends RepositoryTestCase { // we have the old content again checkFile(theFile, "1\n2\n3\n"); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + 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(); + 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"); @@ -226,18 +227,15 @@ public class RebaseCommandTest extends RepositoryTestCase { RebaseResult res = git.rebase().setUpstream("refs/heads/master").call(); assertEquals(Status.OK, res.getStatus()); checkFile(theFile, "1master\n2\n3\ntopic\n"); - assertEquals(lastMasterChange, - new RevWalk(db).parseCommit(db.resolve(Constants.HEAD)) - .getParent(0)); + assertEquals(lastMasterChange, new RevWalk(db).parseCommit( + db.resolve(Constants.HEAD)).getParent(0)); } public void testFilesAddedFromTwoBranches() throws Exception { - Git git = new Git(db); - // create file1 on master - writeTrashFile("file1", "file1"); - git.add().addFilepattern("file1").call(); + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); RevCommit masterCommit = git.commit().setMessage("Add file1 to master") .call(); @@ -256,14 +254,14 @@ public class RebaseCommandTest extends RepositoryTestCase { git.add().addFilepattern("file3").call(); git.commit().setMessage("Add file3 to branch file3").call(); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + 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(), FILE1).exists()); assertTrue(new File(db.getWorkTree(), "file2").exists()); assertTrue(new File(db.getWorkTree(), "file3").exists()); @@ -273,54 +271,40 @@ public class RebaseCommandTest extends RepositoryTestCase { db.resolve(Constants.HEAD)).getParent(0)); checkoutBranch("refs/heads/file2"); - assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), FILE1).exists()); assertTrue(new File(db.getWorkTree(), "file2").exists()); assertFalse(new File(db.getWorkTree(), "file3").exists()); } - public void testAbortOnConflict() throws Exception { - Git git = new Git(db); - + public void testStopOnConflict() throws Exception { // create file1 on master - File theFile = writeTrashFile("file1", "1\n2\n3\n"); - git.add().addFilepattern("file1").call(); - RevCommit firstInMaster = 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(); - git.commit().setMessage("change file1 in master").call(); - + 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(theFile, "1\n2\n3\n"); + checkFile(FILE1, "1", "2", "3"); - assertTrue(new File(db.getWorkTree(), "file1").exists()); // add a line (non-conflicting) - writeTrashFile("file1", "1\n2\n3\ntopic4\n"); - git.add().addFilepattern("file1").call(); - git.commit().setMessage("add a line to file1 in topic").call(); + writeFileAndCommit(FILE1, "add a line to file1 in topic", "1", "2", + "3", "topic4"); // change first line (conflicting) - writeTrashFile("file1", "1topic\n2\n3\ntopic4\n"); - git.add().addFilepattern("file1").call(); - RevCommit conflicting = git.commit() - .setMessage("change file1 in topic").call(); + RevCommit conflicting = writeFileAndCommit(FILE1, + "change file1 in topic", "1topic", "2", "3", "topic4"); - // change second line (not conflicting) - writeTrashFile("file1", "1topic\n2topic\n3\ntopic4\n"); - git.add().addFilepattern("file1").call(); - RevCommit lastTopicCommit = git.commit().setMessage( - "change file1 in topic again").call(); + 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(theFile, - "<<<<<<< OURS\n1master\n=======\n1topic\n>>>>>>> THEIRS\n2\n3\ntopic4\n"); + checkFile(FILE1, + "<<<<<<< OURS\n1master\n=======\n1topic\n>>>>>>> THEIRS\n2\n3\ntopic4"); assertEquals(RepositoryState.REBASING_INTERACTIVE, db .getRepositoryState()); @@ -341,7 +325,7 @@ public class RebaseCommandTest extends RepositoryTestCase { res = git.rebase().setOperation(Operation.ABORT).call(); assertEquals(res.getStatus(), Status.ABORTED); assertEquals("refs/heads/topic", db.getFullBranch()); - checkFile(theFile, "1topic\n2topic\n3\ntopic4\n"); + checkFile(FILE1, "1topic", "2", "3", "topic4"); RevWalk rw = new RevWalk(db); assertEquals(lastTopicCommit, rw .parseCommit(db.resolve(Constants.HEAD))); @@ -351,12 +335,335 @@ public class RebaseCommandTest extends RepositoryTestCase { assertFalse(new File(db.getDirectory(), "rebase-merge").exists()); } - public void testAbortOnConflictFileCreationAndDeletion() throws Exception { - Git git = new Git(db); + 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); + 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()); + } + + 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"); + } + + 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()); + } + + 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()); + } + + 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()); + } + + 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<<<<<<< OURS\n3master\n=======\n3topic\n>>>>>>> THEIRS\n4\n5topic"); + assertEquals(Status.STOPPED, res.getStatus()); + } + + 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); + assertEquals(Status.OK, res.getStatus()); + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + + ObjectId headId = db.resolve(Constants.HEAD); + 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()); + } + + public void testStopOnConflictFileCreationAndDeletion() throws Exception { // create file1 on master - writeTrashFile("file1", "Hello World"); - git.add().addFilepattern("file1").call(); + 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(); @@ -378,10 +685,8 @@ public class RebaseCommandTest extends RepositoryTestCase { 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(); + 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"); @@ -403,9 +708,8 @@ public class RebaseCommandTest extends RepositoryTestCase { deleteTrashFile("file5"); git.add().setUpdate(true).addFilepattern("file5").call(); - RevCommit conflicting = git.commit() - .setMessage("Delete file5, add file folder6 and file7 in topic") - .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()); @@ -429,8 +733,7 @@ public class RebaseCommandTest extends RepositoryTestCase { assertEquals(res.getStatus(), Status.ABORTED); assertEquals("refs/heads/topic", db.getFullBranch()); RevWalk rw = new RevWalk(db); - assertEquals(conflicting, - rw.parseCommit(db.resolve(Constants.HEAD))); + assertEquals(conflicting, rw.parseCommit(db.resolve(Constants.HEAD))); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); // rebase- dir in .git must be deleted @@ -444,6 +747,62 @@ public class RebaseCommandTest extends RepositoryTestCase { } + 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()); + } + + 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 + } + } + private int countPicks() throws IOException { int count = 0; File todoFile = new File(db.getDirectory(), diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index 7c5e98119..07f0cb1d7 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -69,6 +69,7 @@ cannotReadFile=Cannot read file {0} cannotReadHEAD=cannot read HEAD: {0} {1} 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. cannotStoreObjects=cannot store objects cannotUnloadAModifiedTree=Cannot unload a modified tree. @@ -426,6 +427,7 @@ unknownRepositoryFormat=Unknown repository format unknownZlibError=Unknown zlib error. unpackException=Exception while parsing pack stream unmergedPath=Unmerged path: {0} +unmergedPaths=Repository contains unmerged paths unreadablePackIndex=Unreadable pack index: {0} unrecognizedRef=Unrecognized ref: {0} unsupportedCommand0=unsupported command 0 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index e34d0a58b..87053a8c9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -129,6 +129,7 @@ public class JGitText extends TranslationBundle { /***/ public String cannotReadHEAD; /***/ public String cannotReadObject; /***/ public String cannotReadTree; + /***/ public String cannotRebaseWithoutCurrentHead; /***/ public String cannotResolveLocalTrackingRefForUpdating; /***/ public String cannotStoreObjects; /***/ public String cannotUnloadAModifiedTree; @@ -485,6 +486,7 @@ public class JGitText extends TranslationBundle { /***/ public String unknownRepositoryFormat; /***/ public String unknownZlibError; /***/ public String unmergedPath; + /***/ public String unmergedPaths; /***/ public String unpackException; /***/ public String unreadablePackIndex; /***/ public String unrecognizedRef; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index eaa96f578..6629c4f8e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -47,6 +47,7 @@ import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; @@ -55,7 +56,9 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.eclipse.jgit.JGitText; import org.eclipse.jgit.api.RebaseResult.Status; @@ -63,14 +66,18 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; 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.DiffFormatter; +import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; @@ -78,6 +85,8 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.revwalk.RevCommit; 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; @@ -94,6 +103,40 @@ import org.eclipse.jgit.util.RawParseUtils; * >Git documentation about Rebase */ public class RebaseCommand extends GitCommand { + /** + * The name of the "rebase-merge" folder + */ + public static final String REBASE_MERGE = "rebase-merge"; + + /** + * The name of the "stopped-sha" file + */ + public static final String STOPPED_SHA = "stopped-sha"; + + private static final String AUTHOR_SCRIPT = "author-script"; + + private static final String DONE = "done"; + + private static final String GIT_AUTHOR_DATE = "GIT_AUTHOR_DATE"; + + private static final String GIT_AUTHOR_EMAIL = "GIT_AUTHOR_EMAIL"; + + private static final String GIT_AUTHOR_NAME = "GIT_AUTHOR_NAME"; + + private static final String GIT_REBASE_TODO = "git-rebase-todo"; + + private static final String HEAD_NAME = "head-name"; + + private static final String INTERACTIVE = "interactive"; + + private static final String MESSAGE = "message"; + + private static final String ONTO = "onto"; + + private static final String PATCH = "patch"; + + private static final String REBASE_HEAD = "head"; + /** * The available operations */ @@ -132,7 +175,7 @@ public class RebaseCommand extends GitCommand { protected RebaseCommand(Repository repo) { super(repo); walk = new RevWalk(repo); - rebaseDir = new File(repo.getDirectory(), "rebase-merge"); + rebaseDir = new File(repo.getDirectory(), REBASE_MERGE); } /** @@ -145,6 +188,7 @@ public class RebaseCommand extends GitCommand { */ public RebaseResult call() throws NoHeadException, RefNotFoundException, JGitInternalException, GitAPIException { + RevCommit newHead = null; checkCallable(); checkParameters(); try { @@ -158,7 +202,7 @@ public class RebaseCommand extends GitCommand { case SKIP: // fall through case CONTINUE: - String upstreamCommitName = readFile(rebaseDir, "onto"); + String upstreamCommitName = readFile(rebaseDir, ONTO); this.upstreamCommit = walk.parseCommit(repo .resolve(upstreamCommitName)); break; @@ -172,16 +216,13 @@ public class RebaseCommand extends GitCommand { return abort(); if (this.operation == Operation.CONTINUE) - throw new UnsupportedOperationException( - "--continue Not yet implemented"); + newHead = continueRebase(); - if (this.operation == Operation.SKIP) - throw new UnsupportedOperationException( - "--skip Not yet implemented"); + List steps = loadSteps(); - RevCommit newHead = null; + if (this.operation == Operation.SKIP && !steps.isEmpty()) + checkoutCurrentHead(); - List steps = loadSteps(); ObjectReader or = repo.newObjectReader(); int stepsToPop = 0; @@ -211,16 +252,21 @@ public class RebaseCommand extends GitCommand { } stepsToPop++; } - if (newHead != null) { + if (newHead != null || steps.isEmpty()) { // point the previous head (if any) to the new commit - String headName = readFile(rebaseDir, "head-name"); + String headName = readFile(rebaseDir, HEAD_NAME); if (headName.startsWith(Constants.R_REFS)) { RefUpdate rup = repo.updateRef(headName); - rup.setNewObjectId(newHead); - rup.forceUpdate(); + if (newHead != null) { + rup.setNewObjectId(newHead); + rup.forceUpdate(); + } rup = repo.updateRef(Constants.HEAD); rup.link(headName); } + if (this.operation == Operation.SKIP && steps.isEmpty()) { + checkoutCurrentHead(); + } FileUtils.delete(rebaseDir, FileUtils.RECURSIVE); return new RebaseResult(Status.OK); } @@ -230,29 +276,118 @@ public class RebaseCommand extends GitCommand { } } + private void checkoutCurrentHead() throws IOException, NoHeadException, + JGitInternalException { + ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); + if (headTree == null) + throw new NoHeadException( + JGitText.get().cannotRebaseWithoutCurrentHead); + DirCache dc = repo.lockDirCache(); + try { + DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree); + dco.setFailOnConflict(false); + boolean needsDeleteFiles = dco.checkout(); + if (needsDeleteFiles) { + List fileList = dco.getToBeDeleted(); + for (String filePath : fileList) { + File fileToDelete = new File(repo.getWorkTree(), filePath); + if (fileToDelete.exists()) + FileUtils.delete(fileToDelete, FileUtils.RECURSIVE + | FileUtils.RETRY); + } + } + } finally { + dc.unlock(); + } + } + + /** + * @return the commit if we had to do a commit, otherwise null + * @throws GitAPIException + * @throws IOException + */ + private RevCommit continueRebase() throws GitAPIException, IOException { + // if there are still conflicts, we throw a specific Exception + DirCache dc = repo.readDirCache(); + boolean hasUnmergedPaths = dc.hasUnmergedPaths(); + if (hasUnmergedPaths) + throw new UnmergedPathsException(); + + // determine whether we need to commit + TreeWalk treeWalk = new TreeWalk(repo); + treeWalk.reset(); + treeWalk.setRecursive(true); + treeWalk.addTree(new DirCacheIterator(dc)); + ObjectId id = repo.resolve(Constants.HEAD + "^{tree}"); + if (id == null) + throw new NoHeadException( + JGitText.get().cannotRebaseWithoutCurrentHead); + + treeWalk.addTree(id); + + treeWalk.setFilter(TreeFilter.ANY_DIFF); + + boolean needsCommit = treeWalk.next(); + treeWalk.release(); + + if (needsCommit) { + CommitCommand commit = new Git(repo).commit(); + commit.setMessage(readFile(rebaseDir, MESSAGE)); + commit.setAuthor(parseAuthor()); + return commit.call(); + } + return null; + } + + private PersonIdent parseAuthor() throws IOException { + File authorScriptFile = new File(rebaseDir, AUTHOR_SCRIPT); + byte[] raw; + try { + raw = IO.readFully(authorScriptFile); + } catch (FileNotFoundException notFound) { + return null; + } + return parseAuthor(raw); + } + private RebaseResult stop(RevCommit commitToPick) throws IOException { - StringBuilder sb = new StringBuilder(100); - sb.append("GIT_AUTHOR_NAME='"); - sb.append(commitToPick.getAuthorIdent().getName()); - sb.append("'\n"); - sb.append("GIT_AUTHOR_EMAIL='"); - sb.append(commitToPick.getAuthorIdent().getEmailAddress()); - sb.append("'\n"); - sb.append("GIT_AUTHOR_DATE='"); - sb.append(commitToPick.getAuthorIdent().getWhen()); - sb.append("'\n"); - createFile(rebaseDir, "author-script", sb.toString()); - createFile(rebaseDir, "message", commitToPick.getShortMessage()); + PersonIdent author = commitToPick.getAuthorIdent(); + String authorScript = toAuthorScript(author); + createFile(rebaseDir, AUTHOR_SCRIPT, authorScript); + createFile(rebaseDir, MESSAGE, commitToPick.getFullMessage()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DiffFormatter df = new DiffFormatter(bos); df.setRepository(repo); df.format(commitToPick.getParent(0), commitToPick); - createFile(rebaseDir, "patch", new String(bos.toByteArray(), "UTF-8")); - createFile(rebaseDir, "stopped-sha", repo.newObjectReader().abbreviate( + createFile(rebaseDir, PATCH, new String(bos.toByteArray(), + Constants.CHARACTER_ENCODING)); + createFile(rebaseDir, STOPPED_SHA, repo.newObjectReader().abbreviate( commitToPick).name()); return new RebaseResult(commitToPick); } + String toAuthorScript(PersonIdent author) { + StringBuilder sb = new StringBuilder(100); + sb.append(GIT_AUTHOR_NAME); + sb.append("='"); + sb.append(author.getName()); + sb.append("'\n"); + sb.append(GIT_AUTHOR_EMAIL); + sb.append("='"); + sb.append(author.getEmailAddress()); + sb.append("'\n"); + // the command line uses the "external String" + // representation for date and timezone + sb.append(GIT_AUTHOR_DATE); + sb.append("='"); + String externalString = author.toExternalString(); + sb + .append(externalString.substring(externalString + .lastIndexOf('>') + 2)); + sb.append("'\n"); + return sb.toString(); + } + /** * Removes the number of lines given in the parameter from the * git-rebase-todo file but preserves comments and other lines @@ -266,10 +401,10 @@ public class RebaseCommand extends GitCommand { return; List todoLines = new ArrayList(); List poppedLines = new ArrayList(); - File todoFile = new File(rebaseDir, "git-rebase-todo"); - File doneFile = new File(rebaseDir, "done"); + File todoFile = new File(rebaseDir, GIT_REBASE_TODO); + File doneFile = new File(rebaseDir, DONE); BufferedReader br = new BufferedReader(new InputStreamReader( - new FileInputStream(todoFile), "UTF-8")); + new FileInputStream(todoFile), Constants.CHARACTER_ENCODING)); try { // check if the line starts with a action tag (pick, skip...) while (poppedLines.size() < numSteps) { @@ -297,7 +432,7 @@ public class RebaseCommand extends GitCommand { } BufferedWriter todoWriter = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(todoFile), "UTF-8")); + new FileOutputStream(todoFile), Constants.CHARACTER_ENCODING)); try { for (String writeLine : todoLines) { todoWriter.write(writeLine); @@ -311,7 +446,8 @@ public class RebaseCommand extends GitCommand { // append here BufferedWriter doneWriter = new BufferedWriter( new OutputStreamWriter( - new FileOutputStream(doneFile, true), "UTF-8")); + new FileOutputStream(doneFile, true), + Constants.CHARACTER_ENCODING)); try { for (String writeLine : poppedLines) { doneWriter.write(writeLine); @@ -363,14 +499,14 @@ public class RebaseCommand extends GitCommand { // create the folder for the meta information rebaseDir.mkdir(); - createFile(repo.getDirectory(), "ORIG_HEAD", headId.name()); - createFile(rebaseDir, "head", headId.name()); - createFile(rebaseDir, "head-name", headName); - createFile(rebaseDir, "onto", upstreamCommit.name()); - createFile(rebaseDir, "interactive", ""); + createFile(repo.getDirectory(), Constants.ORIG_HEAD, headId.name()); + createFile(rebaseDir, REBASE_HEAD, headId.name()); + createFile(rebaseDir, HEAD_NAME, headName); + createFile(rebaseDir, ONTO, upstreamCommit.name()); + createFile(rebaseDir, INTERACTIVE, ""); BufferedWriter fw = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(new File(rebaseDir, "git-rebase-todo")), - "UTF-8")); + new FileOutputStream(new File(rebaseDir, GIT_REBASE_TODO)), + Constants.CHARACTER_ENCODING)); fw.write("# Created by EGit: rebasing " + upstreamCommit.name() + " onto " + headId.name()); fw.newLine(); @@ -404,13 +540,7 @@ public class RebaseCommand extends GitCommand { if (this.operation != Operation.BEGIN) { // these operations are only possible while in a rebasing state switch (repo.getRepositoryState()) { - case REBASING: - // fall through case REBASING_INTERACTIVE: - // fall through - case REBASING_MERGE: - // fall through - case REBASING_REBASING: break; default: throw new WrongRepositoryStateException(MessageFormat.format( @@ -438,7 +568,7 @@ public class RebaseCommand extends GitCommand { File file = new File(parentDir, name); FileOutputStream fos = new FileOutputStream(file); try { - fos.write(content.getBytes("UTF-8")); + fos.write(content.getBytes(Constants.CHARACTER_ENCODING)); fos.write('\n'); } finally { fos.close(); @@ -447,7 +577,7 @@ public class RebaseCommand extends GitCommand { private RebaseResult abort() throws IOException { try { - String commitId = readFile(repo.getDirectory(), "ORIG_HEAD"); + String commitId = readFile(repo.getDirectory(), Constants.ORIG_HEAD); monitor.beginTask(MessageFormat.format( JGitText.get().abortingRebase, commitId), ProgressMonitor.UNKNOWN); @@ -463,7 +593,7 @@ public class RebaseCommand extends GitCommand { monitor.endTask(); } try { - String headName = readFile(rebaseDir, "head-name"); + String headName = readFile(rebaseDir, HEAD_NAME); if (headName.startsWith(Constants.R_REFS)) { monitor.beginTask(MessageFormat.format( JGitText.get().resettingHead, headName), @@ -527,7 +657,7 @@ public class RebaseCommand extends GitCommand { } private List loadSteps() throws IOException { - byte[] buf = IO.readFully(new File(rebaseDir, "git-rebase-todo")); + byte[] buf = IO.readFully(new File(rebaseDir, GIT_REBASE_TODO)); int ptr = 0; int tokenBegin = 0; ArrayList r = new ArrayList(); @@ -656,4 +786,42 @@ public class RebaseCommand extends GitCommand { this.action = action; } } + + PersonIdent parseAuthor(byte[] raw) { + if (raw.length == 0) + return null; + + Map keyValueMap = new HashMap(); + for (int p = 0; p < raw.length;) { + int end = RawParseUtils.nextLF(raw, p); + if (end == p) + break; + int equalsIndex = RawParseUtils.next(raw, p, '='); + if (equalsIndex == end) + break; + String key = RawParseUtils.decode(raw, p, equalsIndex - 1); + String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2); + p = end; + keyValueMap.put(key, value); + } + + String name = keyValueMap.get(GIT_AUTHOR_NAME); + String email = keyValueMap.get(GIT_AUTHOR_EMAIL); + String time = keyValueMap.get(GIT_AUTHOR_DATE); + + // the time is saved as + long when = Long.parseLong(time.substring(0, time.indexOf(' '))) * 1000; + String tzOffsetString = time.substring(time.indexOf(' ') + 1); + int multiplier = -1; + if (tzOffsetString.charAt(0) == '+') + multiplier = 1; + int hours = Integer.parseInt(tzOffsetString.substring(1, 3)); + int minutes = Integer.parseInt(tzOffsetString.substring(3, 5)); + // this is in format (+/-)HHMM (hours and minutes) + // we need to convert into minutes + int tz = (hours * 60 + minutes) * multiplier; + if (name != null && email != null) + return new PersonIdent(name, email, when, tz); + return null; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java new file mode 100644 index 000000000..ab078663b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010,Mathias Kinzler 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; + +import org.eclipse.jgit.JGitText; + +/** + * Thrown when branch deletion fails due to unmerged data + */ +public class UnmergedPathsException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * The default constructor with a default message + */ + public UnmergedPathsException() { + super(JGitText.get().unmergedPaths); + } +}