From 5b0e73b849d19c9f072c4c6738a5d5adae413112 Mon Sep 17 00:00:00 2001 From: Stefan Lay Date: Thu, 20 May 2010 15:09:39 +0200 Subject: [PATCH] Add a merge command to the jgit API Merges the current head with one other commit. In this first iteration the merge command supports only fast forward and already up-to-date. Change-Id: I0db480f061e01b343570cf7da02cac13a0cbdf8f Signed-off-by: Stefan Lay Signed-off-by: Christian Halstrick Signed-off-by: Chris Aniszczyk --- .../eclipse/jgit/api/MergeCommandTest.java | 183 +++++++++++++ .../org/eclipse/jgit/JGitText.properties | 6 + .../src/org/eclipse/jgit/JGitText.java | 6 + .../jgit/api/CheckoutConflictException.java | 85 ++++++ .../src/org/eclipse/jgit/api/Git.java | 13 + .../jgit/api/InvalidMergeHeadsException.java | 53 ++++ .../org/eclipse/jgit/api/MergeCommand.java | 249 ++++++++++++++++++ .../src/org/eclipse/jgit/api/MergeResult.java | 150 +++++++++++ 8 files changed, 745 insertions(+) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutConflictException.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/InvalidMergeHeadsException.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java new file mode 100644 index 000000000..c965c6766 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2010, Stefan Lay + * Copyright (C) 2010, Christian Halstrick + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.errors.CheckoutConflictException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.GitIndex; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RepositoryTestCase; +import org.eclipse.jgit.lib.WorkDirCheckout; +import org.eclipse.jgit.lib.GitIndex.Entry; +import org.eclipse.jgit.revwalk.RevCommit; + +public class MergeCommandTest extends RepositoryTestCase { + + public void testMergeInItself() throws Exception { + Git git = new Git(db); + git.commit().setMessage("initial commit").call(); + + MergeResult result = git.merge().include(db.getRef(Constants.HEAD)).call(); + assertEquals(MergeResult.MergeStatus.ALREADY_UP_TO_DATE, result.getMergeStatus()); + } + + public void testAlreadyUpToDate() throws Exception { + Git git = new Git(db); + RevCommit first = git.commit().setMessage("initial commit").call(); + createBranch(first, "refs/heads/branch1"); + + RevCommit second = git.commit().setMessage("second commit").call(); + MergeResult result = git.merge().include(db.getRef("refs/heads/branch1")).call(); + assertEquals(MergeResult.MergeStatus.ALREADY_UP_TO_DATE, result.getMergeStatus()); + assertEquals(second, result.getNewHead()); + + } + + public void testFastForward() throws Exception { + Git git = new Git(db); + RevCommit first = git.commit().setMessage("initial commit").call(); + createBranch(first, "refs/heads/branch1"); + + RevCommit second = git.commit().setMessage("second commit").call(); + + checkoutBranch("refs/heads/branch1"); + + MergeResult result = git.merge().include(db.getRef(Constants.MASTER)).call(); + + assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus()); + assertEquals(second, result.getNewHead()); + } + + public void testFastForwardWithFiles() throws Exception { + Git git = new Git(db); + + addNewFileToIndex("file1"); + RevCommit first = git.commit().setMessage("initial commit").call(); + + assertTrue(new File(db.getWorkDir(), "file1").exists()); + createBranch(first, "refs/heads/branch1"); + + addNewFileToIndex("file2"); + RevCommit second = git.commit().setMessage("second commit").call(); + assertTrue(new File(db.getWorkDir(), "file2").exists()); + + checkoutBranch("refs/heads/branch1"); + assertFalse(new File(db.getWorkDir(), "file2").exists()); + + MergeResult result = git.merge().include(db.getRef(Constants.MASTER)).call(); + + assertTrue(new File(db.getWorkDir(), "file1").exists()); + assertTrue(new File(db.getWorkDir(), "file2").exists()); + assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus()); + assertEquals(second, result.getNewHead()); + } + + public void testMultipleHeads() throws Exception { + Git git = new Git(db); + + addNewFileToIndex("file1"); + RevCommit first = git.commit().setMessage("initial commit").call(); + createBranch(first, "refs/heads/branch1"); + + addNewFileToIndex("file2"); + RevCommit second = git.commit().setMessage("second commit").call(); + + addNewFileToIndex("file3"); + git.commit().setMessage("third commit").call(); + + checkoutBranch("refs/heads/branch1"); + assertFalse(new File(db.getWorkDir(), "file2").exists()); + assertFalse(new File(db.getWorkDir(), "file3").exists()); + + MergeCommand merge = git.merge(); + merge.include(second.getId()); + merge.include(db.getRef(Constants.MASTER)); + try { + merge.call(); + fail("Expected exception not thrown when merging multiple heads"); + } catch (InvalidMergeHeadsException e) { + } + } + + private void createBranch(ObjectId objectId, String branchName) throws IOException { + RefUpdate updateRef = db.updateRef(branchName); + updateRef.setNewObjectId(objectId); + updateRef.update(); + } + + private void checkoutBranch(String branchName) throws Exception { + File workDir = db.getWorkDir(); + if (workDir != null) { + WorkDirCheckout workDirCheckout = new WorkDirCheckout(db, + workDir, db.mapCommit(Constants.HEAD).getTree(), + db.getIndex(), db.mapCommit(branchName).getTree()); + workDirCheckout.setFailOnConflict(true); + try { + workDirCheckout.checkout(); + } catch (CheckoutConflictException e) { + throw new JGitInternalException( + "Couldn't check out because of conflicts", e); + } + } + + // update the HEAD + RefUpdate refUpdate = db.updateRef(Constants.HEAD); + refUpdate.link(branchName); + } + + private void addNewFileToIndex(String filename) throws IOException, + CorruptObjectException { + File writeTrashFile = writeTrashFile(filename, filename); + + GitIndex index = db.getIndex(); + Entry entry = index.add(db.getWorkDir(), writeTrashFile); + entry.update(writeTrashFile); + index.write(); + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index e9ed28a19..b769671c7 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -100,6 +100,7 @@ corruptObjectNoType=no type corruptObjectNotree=no tree corruptObjectPackfileChecksumIncorrect=Packfile checksum incorrect. corruptionDetectedReReadingAt=Corruption detected re-reading at {0} +couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen couldNotLockHEAD=Could not lock HEAD @@ -137,6 +138,7 @@ errorListing=Error listing {0} errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on the remote end: {0} errorReadingInfoRefs=error reading info/refs exceptionCaughtDuringExecutionOfCommitCommand=Exception caught during execution of commit command +exceptionCaughtDuringExecutionOfMergeCommand=Exception caught during execution of merge command. {0} exceptionOccuredDuringAddingOfOptionToALogCommand=Exception occured during adding of {0} as option to a Log command exceptionOccuredDuringReadingOfGIT_DIR=Exception occured during reading of $GIT_DIR/{0}. {1} expectedACKNAKFoundEOF=Expected ACK/NAK, found EOF @@ -215,6 +217,8 @@ lockOnNotClosed=Lock on {0} not closed. lockOnNotHeld=Lock on {0} not held. malformedpersonIdentString=Malformed PersonIdent string (no < was found): {0} mergeStrategyAlreadyExistsAsDefault=Merge strategy "{0}" already exists as a default strategy +mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads to be merged into HEAD +mergeUsingStrategyResultedInDescription=Merge using strategy {0} resulted in: {1}. {2} missingAccesskey=Missing accesskey. missingForwardImageInGITBinaryPatch=Missing forward-image in GIT binary patch missingObject=Missing {0} {1} @@ -231,6 +235,7 @@ noApplyInDelete=No apply in delete noClosingBracket=No closing {0} found for {1} at index {2}. noHEADExistsAndNoExplicitStartingRevisionWasSpecified=No HEAD exists and no explicit starting revision was specified noHMACsupport=No {0} support: {1} +noMergeHeadSpecified=No merge head specified noSuchRef=no such ref noXMLParserAvailable=No XML parser available. notABoolean=Not a boolean: {0} @@ -251,6 +256,7 @@ objectIsCorrupt=Object {0} is corrupt: {1} objectIsNotA=Object {0} is not a {1}. objectNotFoundIn=Object {0} not found in {1}. offsetWrittenDeltaBaseForObjectNotFoundInAPack=Offset-written delta base for object not found in a pack +onlyAlreadyUpToDateAndFastForwardMergesAreAvailable=only already-up-to-date and fast forward merges are available onlyOneFetchSupported=Only one fetch supported onlyOneOperationCallPerConnectionIsSupported=Only one operation call per connection is supported. openFilesMustBeAtLeast1=Open files must be >= 1 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index a7c2e685c..49056c7b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -160,6 +160,7 @@ public static JGitText get() { /***/ public String corruptObjectNotree; /***/ public String corruptObjectPackfileChecksumIncorrect; /***/ public String corruptionDetectedReReadingAt; + /***/ public String couldNotCheckOutBecauseOfConflicts; /***/ public String couldNotDeleteLockFileShouldNotHappen; /***/ public String couldNotDeleteTemporaryIndexFileShouldNotHappen; /***/ public String couldNotLockHEAD; @@ -197,6 +198,7 @@ public static JGitText get() { /***/ public String errorOccurredDuringUnpackingOnTheRemoteEnd; /***/ public String errorReadingInfoRefs; /***/ public String exceptionCaughtDuringExecutionOfCommitCommand; + /***/ public String exceptionCaughtDuringExecutionOfMergeCommand; /***/ public String exceptionOccuredDuringAddingOfOptionToALogCommand; /***/ public String exceptionOccuredDuringReadingOfGIT_DIR; /***/ public String expectedACKNAKFoundEOF; @@ -275,6 +277,8 @@ public static JGitText get() { /***/ public String lockOnNotHeld; /***/ public String malformedpersonIdentString; /***/ public String mergeStrategyAlreadyExistsAsDefault; + /***/ public String mergeStrategyDoesNotSupportHeads; + /***/ public String mergeUsingStrategyResultedInDescription; /***/ public String missingAccesskey; /***/ public String missingForwardImageInGITBinaryPatch; /***/ public String missingObject; @@ -291,6 +295,7 @@ public static JGitText get() { /***/ public String noClosingBracket; /***/ public String noHEADExistsAndNoExplicitStartingRevisionWasSpecified; /***/ public String noHMACsupport; + /***/ public String noMergeHeadSpecified; /***/ public String noSuchRef; /***/ public String noXMLParserAvailable; /***/ public String notABoolean; @@ -311,6 +316,7 @@ public static JGitText get() { /***/ public String objectIsNotA; /***/ public String objectNotFoundIn; /***/ public String offsetWrittenDeltaBaseForObjectNotFoundInAPack; + /***/ public String onlyAlreadyUpToDateAndFastForwardMergesAreAvailable; /***/ public String onlyOneFetchSupported; /***/ public String onlyOneOperationCallPerConnectionIsSupported; /***/ public String openFilesMustBeAtLeast1; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutConflictException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutConflictException.java new file mode 100644 index 000000000..09dda6f61 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutConflictException.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010, Christian Halstrick and + * other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v1.0 which accompanies this + * distribution, is reproduced below, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.util.LinkedList; +import java.util.List; + +/** + * Exception thrown when a command can't succeed because of unresolved + * conflicts. + */ +public class CheckoutConflictException extends GitAPIException { + private static final long serialVersionUID = 1L; + private List conflictingPaths; + + CheckoutConflictException(String message, Throwable cause) { + super(message, cause); + } + + CheckoutConflictException(String message, List conflictingPaths, Throwable cause) { + super(message, cause); + this.conflictingPaths = conflictingPaths; + } + + CheckoutConflictException(String message) { + super(message); + } + + CheckoutConflictException(String message, List conflictingPaths) { + super(message); + this.conflictingPaths = conflictingPaths; + } + + /** @return all the paths where unresolved conflicts have been detected */ + public List getConflictingPaths() { + return conflictingPaths; + } + + /** + * Adds a new conflicting path + * @param conflictingPath + * @return {@code this} + */ + CheckoutConflictException addConflictingPath(String conflictingPath) { + if (conflictingPaths == null) + conflictingPaths = new LinkedList(); + conflictingPaths.add(conflictingPath); + return this; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 30dcbd75b..28946e5fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -119,6 +119,19 @@ public LogCommand log() { return new LogCommand(repo); } + /** + * Returns a command class to execute a {@code Merge} command + * + * @see Git documentation about Merge + * @return a {@link MergeCommand} used to collect all optional parameters + * and to finally execute the {@code Merge} command + */ + public MergeCommand merge() { + return new MergeCommand(repo); + } + /** * @return the git repository this class is interacting with */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/InvalidMergeHeadsException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/InvalidMergeHeadsException.java new file mode 100644 index 000000000..774885391 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/InvalidMergeHeadsException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010, Christian Halstrick 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; + + +/** + * Exception thrown when a merge command was called without specifying the + * proper amount/type of merge heads. E.g. a non-octopus merge strategy was + * confronted with more than one head to be merged into HEAD. Another + * case would be if a merge was called without including any head. + */ +public class InvalidMergeHeadsException extends GitAPIException { + private static final long serialVersionUID = 1L; + + InvalidMergeHeadsException(String msg) { + super(msg); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java new file mode 100644 index 000000000..00a030915 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2010, Christian Halstrick + * Copyright (C) 2010, Stefan Lay + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.LinkedList; +import java.util.List; + +import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.api.MergeResult.MergeStatus; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.GitIndex; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WorkDirCheckout; +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * A class used to execute a {@code Merge} command. It has setters for all + * supported options and arguments of this command and a {@link #call()} method + * to finally execute the command. Each instance of this class should only be + * used for one invocation of the command (means: one call to {@link #call()}) + *

+ * This is currently a very basic implementation which takes only one commits to + * merge with as option. Furthermore it does supports only fast forward. + * + * @see Git documentation about Merge + */ +public class MergeCommand extends GitCommand { + + private MergeStrategy mergeStrategy = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE; + + private List commits = new LinkedList(); + + /** + * @param repo + */ + protected MergeCommand(Repository repo) { + super(repo); + } + + /** + * Executes the {@code Merge} command with all the options and parameters + * collected by the setter methods (e.g. {@link #include(Ref)}) of this + * class. Each instance of this class should only be used for one invocation + * of the command. Don't call this method twice on an instance. + * + * @return the result of the merge + */ + public MergeResult call() throws NoHeadException, + ConcurrentRefUpdateException, CheckoutConflictException, + InvalidMergeHeadsException { + checkCallable(); + + if (commits.size() != 1) + throw new InvalidMergeHeadsException( + commits.isEmpty() ? JGitText.get().noMergeHeadSpecified + : MessageFormat.format( + JGitText.get().mergeStrategyDoesNotSupportHeads, + mergeStrategy.getName(), commits.size())); + + try { + Ref head = repo.getRef(Constants.HEAD); + if (head == null) + throw new NoHeadException(JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported); + StringBuilder refLogMessage = new StringBuilder("merge "); + + // Check for FAST_FORWARD, ALREADY_UP_TO_DATE + RevWalk revWalk = new RevWalk(repo); + RevCommit headCommit = revWalk.lookupCommit(head.getObjectId()); + + Ref ref = commits.get(0); + + refLogMessage.append(ref.getName()); + + // handle annotated tags + ObjectId objectId = ref.getPeeledObjectId(); + if (objectId == null) + objectId = ref.getObjectId(); + + RevCommit srcCommit = revWalk.lookupCommit(objectId); + if (revWalk.isMergedInto(srcCommit, headCommit)) { + setCallable(false); + return new MergeResult(headCommit, + MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy); + } else if (revWalk.isMergedInto(headCommit, srcCommit)) { + // FAST_FORWARD detected: skip doing a real merge but only + // update HEAD + refLogMessage.append(": " + MergeStatus.FAST_FORWARD); + checkoutNewHead(revWalk, headCommit, srcCommit); + updateHead(refLogMessage, srcCommit, head.getObjectId()); + setCallable(false); + return new MergeResult(srcCommit, MergeStatus.FAST_FORWARD, + mergeStrategy); + } else { + return new MergeResult( + headCommit, + MergeResult.MergeStatus.NOT_SUPPORTED, + mergeStrategy, + JGitText.get().onlyAlreadyUpToDateAndFastForwardMergesAreAvailable); + } + } catch (IOException e) { + throw new JGitInternalException( + MessageFormat.format( + JGitText.get().exceptionCaughtDuringExecutionOfMergeCommand, + e)); + } + } + + private void checkoutNewHead(RevWalk revWalk, RevCommit headCommit, + RevCommit newHeadCommit) throws IOException, CheckoutConflictException { + GitIndex index = repo.getIndex(); + + File workDir = repo.getWorkDir(); + if (workDir != null) { + WorkDirCheckout workDirCheckout = new WorkDirCheckout(repo, + workDir, headCommit.asCommit(revWalk).getTree(), index, + newHeadCommit.asCommit(revWalk).getTree()); + workDirCheckout.setFailOnConflict(true); + try { + workDirCheckout.checkout(); + } catch (org.eclipse.jgit.errors.CheckoutConflictException e) { + throw new CheckoutConflictException( + JGitText.get().couldNotCheckOutBecauseOfConflicts, + workDirCheckout.getConflicts(), e); + } + index.write(); + } + } + + private void updateHead(StringBuilder refLogMessage, + ObjectId newHeadId, ObjectId oldHeadID) throws IOException, + ConcurrentRefUpdateException { + RefUpdate refUpdate = repo.updateRef(Constants.HEAD); + refUpdate.setNewObjectId(newHeadId); + refUpdate.setRefLogMessage(refLogMessage.toString(), false); + refUpdate.setExpectedOldObjectId(oldHeadID); + Result rc = refUpdate.update(); + switch (rc) { + case NEW: + case FAST_FORWARD: + return; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException( + JGitText.get().couldNotLockHEAD, refUpdate.getRef(), rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, Constants.HEAD, + newHeadId.toString(), rc)); + } + } + + /** + * + * @param mergeStrategy + * the {@link MergeStrategy} to be used + * @return {@code this} + */ + public MergeCommand setStrategy(MergeStrategy mergeStrategy) { + checkCallable(); + this.mergeStrategy = mergeStrategy; + return this; + } + + /** + * @param commit + * a reference to a commit which is merged with the current + * head + * @return {@code this} + */ + public MergeCommand include(Ref commit) { + checkCallable(); + commits.add(commit); + return this; + } + + /** + * @param commit + * the Id of a commit which is merged with the current head + * @return {@code this} + */ + public MergeCommand include(AnyObjectId commit) { + return include(commit.getName(), commit); + } + + /** + * @param name a name given to the commit + * @param commit + * the Id of a commit which is merged with the current head + * @return {@code this} + */ + public MergeCommand include(String name, AnyObjectId commit) { + return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name, + commit.copy())); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java new file mode 100644 index 000000000..a293ad0c9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2010, Stefan Lay + * Copyright (C) 2010, Christian Halstrick + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.text.MessageFormat; + +import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.merge.MergeStrategy; + +/** + * Encapsulates the result of a {@link MergeCommand}. + */ +public class MergeResult { + + /** + * The status the merge resulted in. + */ + public enum MergeStatus { + /** */ + FAST_FORWARD { + @Override + public String toString() { + return "Fast-forward"; + } + }, + /** */ + ALREADY_UP_TO_DATE { + public String toString() { + return "Already-up-to-date"; + } + }, + /** */ + FAILED { + public String toString() { + return "Failed"; + } + }, + /** */ + MERGED { + public String toString() { + return "Merged"; + } + }, + /** */ + NOT_SUPPORTED { + public String toString() { + return "Not-yet-supported"; + } + } + } + + private ObjectId newHead; + + private MergeStatus mergeStatus; + + private String description; + + private MergeStrategy mergeStrategy; + + /** + * @param newHead the object the head points at after the merge + * @param mergeStatus the status the merge resulted in + * @param mergeStrategy the used {@link MergeStrategy} + */ + public MergeResult(ObjectId newHead, MergeStatus mergeStatus, + MergeStrategy mergeStrategy) { + this.newHead = newHead; + this.mergeStatus = mergeStatus; + this.mergeStrategy = mergeStrategy; + } + + /** + * @param newHead the object the head points at after the merge + * @param mergeStatus the status the merge resulted in + * @param mergeStrategy the used {@link MergeStrategy} + * @param description a user friendly description of the merge result + */ + public MergeResult(ObjectId newHead, MergeStatus mergeStatus, + MergeStrategy mergeStrategy, String description) { + this.newHead = newHead; + this.mergeStatus = mergeStatus; + this.mergeStrategy = mergeStrategy; + this.description = description; + } + + /** + * @return the object the head points at after the merge + */ + public ObjectId getNewHead() { + return newHead; + } + + /** + * @return the status the merge resulted in + */ + public MergeStatus getMergeStatus() { + return mergeStatus; + } + + @Override + public String toString() { + return MessageFormat.format( + JGitText.get().mergeUsingStrategyResultedInDescription, + mergeStrategy.getName(), mergeStatus, (description == null ? "" + : ", " + description)); + } + +}