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 <stefan.lay@sap.com>
Signed-off-by: Christian Halstrick <christian.halstrick@sap.com>
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
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 0000000..c965c67
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
+ * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import 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 e9ed28a..b769671 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 @@
 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 @@
 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 @@
 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 @@
 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 @@
 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 a7c2e68..49056c7 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 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 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 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 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 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 0000000..09dda6f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutConflictException.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and
+ * other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v1.0 which accompanies this
+ * distribution, is reproduced below, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import 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<String> conflictingPaths;
+
+	CheckoutConflictException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	CheckoutConflictException(String message, List<String> conflictingPaths, Throwable cause) {
+		super(message, cause);
+		this.conflictingPaths = conflictingPaths;
+	}
+
+	CheckoutConflictException(String message) {
+		super(message);
+	}
+
+	CheckoutConflictException(String message, List<String> conflictingPaths) {
+		super(message);
+		this.conflictingPaths = conflictingPaths;
+	}
+
+	/** @return all the paths where unresolved conflicts have been detected */
+	public List<String> getConflictingPaths() {
+		return conflictingPaths;
+	}
+
+	/**
+	 * Adds a new conflicting path
+	 * @param conflictingPath
+	 * @return {@code this}
+	 */
+	CheckoutConflictException addConflictingPath(String conflictingPath) {
+		if (conflictingPaths == null)
+			conflictingPaths = new LinkedList<String>();
+		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 30dcbd7..28946e5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -120,6 +120,19 @@
 	}
 
 	/**
+	 * Returns a command class to execute a {@code Merge} command
+	 *
+	 * @see <a
+	 *      href="http://www.kernel.org/pub/software/scm/git/docs/git-merge.html"
+	 *      >Git documentation about Merge</a>
+	 * @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
 	 */
 	public Repository getRepository() {
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 0000000..7748853
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/InvalidMergeHeadsException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010, Christian Halstrick <christian.halstrick@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;
+
+
+/**
+ * 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 0000000..00a0309
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
+ * Copyright (C) 2010, 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;
+
+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()})
+ * <p>
+ * 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 <a href="http://www.kernel.org/pub/software/scm/git/docs/git-merge.html"
+ *      >Git documentation about Merge</a>
+ */
+public class MergeCommand extends GitCommand<MergeResult> {
+
+	private MergeStrategy mergeStrategy = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE;
+
+	private List<Ref> commits = new LinkedList<Ref>();
+
+	/**
+	 * @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 0000000..a293ad0
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
+ * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import 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));
+	}
+
+}