Add merge feature into /changes REST endpoint

Allow user to create a merge in Gerrit via POST /changes REST API.
By adding a "merge" attribute which could be a SHA1, a branch name or a
tag name, etc. in the post JSON, gerrit will create a merge commit
instead of an empty commit. If there are conflicts, the response will
be rejected with a MergeConfictException which contains conflict
message.

Add dry run end point into GET
/projects/{project}/branches/{branch}/mergeable with query parameters
source (required) and strategy (optional) and return a
MergeableInfo entity.

Change-Id: I8f45f324704b3ff3eb20cb57c6e3bd75f2bf60ef
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ca5d460..9eb33fb 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3057,7 +3057,8 @@
   )]}'
   {
     submit_type: "MERGE_IF_NECESSARY",
-    mergeable: true,
+    strategy: "recursive",
+    mergeable: true
   }
 ----
 
@@ -4248,6 +4249,8 @@
 change operation.
 |`new_branch`         |optional, default to `false`|
 Allow creating a new branch when set to `true`.
+|`merge`              |optional|
+The detail of a merge commit as a link:#merge-input[MergeInput] entity.
 |==================================
 
 [[change-message-info]]
@@ -4733,12 +4736,33 @@
 Submit type used for this change, can be `MERGE_IF_NECESSARY`,
 `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
 `CHERRY_PICK`.
+|`merge_strategy`     |optional|
+The strategy of the merge, can be `recursive`, `resolve`,
+`simple-two-way-in-core`, `ours` or `theirs`.
 |`mergeable`     ||
 `true` if this change is cleanly mergeable, `false` otherwise
+|`conflicts`|optional|
+A list of paths with conflicts
 |`mergeable_into`|optional|
 A list of other branch names where this change could merge cleanly
 |============================
 
+[[merge-input]]
+=== MergeInput
+The `MergeInput` entity contains information about the merge
+
+[options="header",cols="1,^1,5"]
+|============================
+|Field Name      ||Description
+|`source`   ||
+The source to merge from, e.g. a complete or abbreviated commit SHA-1,
+a complete reference name, a short reference name under refs/heads, refs/tags,
+or refs/remotes namespace, etc.
+|`strategy`     |optional|
+The strategy of the merge, can be `recursive`, `resolve`,
+`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
+|============================
+
 [[move-input]]
 === MoveInput
 The `MoveInput` entity contains information for moving a change to a new branch.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 7d4b92f..21a7ac7 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1354,6 +1354,42 @@
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
 
+
+[[get-mergeable-info]]
+=== Get Mergeable Information
+--
+'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]/mergeable'
+--
+
+Gets whether the source is mergeable with the target branch.
+
+The `source` query parameter is required, which can be anything that could be resolved
+to a commit, see examples in link:rest-api-changes.html#merge-input[MergeInput].
+
+Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`,
+`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
+
+.Request
+----
+  GET /projects/test/branches/master/mergeable?source=testbranch&strategy=recursive HTTP/1.0
+----
+
+As response a link:#mergeable-info[MergeableInfo] entity is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    submit_type: "MERGE_IF_NECESSARY",
+    merge_strategy: "recursive",
+    mergeable: true
+  }
+----
+
 [[get-reflog]]
 === Get Reflog
 --
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 7179e80..a55acf2 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -206,6 +206,11 @@
     }
   }
 
+  public void setParent(RevCommit parent) throws Exception {
+    commitBuilder.noParents();
+    commitBuilder.parent(parent);
+  }
+
   public Result to(String ref) throws Exception {
     commitBuilder.add(fileName, content);
     return execute(ref);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 13f5063..25628c2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -22,14 +22,19 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.testutil.ConfigSuite;
@@ -191,4 +196,81 @@
 
     assertThat(o.signedOffBy).isTrue();
   }
+
+
+  @Test
+  public void createMergeChange() throws Exception {
+    changeInTwoBranches("a.txt", "b.txt");
+    ChangeInput in =
+        newMergeChangeInput("master", "branchA", ChangeStatus.NEW);
+    assertCreateSucceeds(in);
+  }
+
+  @Test
+  public void createMergeChange_Conflicts() throws Exception {
+    changeInTwoBranches("shared.txt", "shared.txt");
+    ChangeInput in =
+        newMergeChangeInput("master", "branchA", ChangeStatus.NEW);
+    assertCreateFails(in, RestApiException.class, "merge conflict");
+  }
+
+  @Test
+  public void dryRunMerge() throws Exception {
+    changeInTwoBranches("a.txt", "b.txt");
+    RestResponse r = adminRestSession.get("/projects/" + project.get()
+        + "/branches/master/mergeable?source=branchA");
+    MergeableInfo m = newGson().fromJson(r.getReader(), MergeableInfo.class);
+    assertThat(m.mergeable).isTrue();
+  }
+
+  @Test
+  public void dryRunMerge_Conflicts() throws Exception {
+    changeInTwoBranches("a.txt", "a.txt");
+    RestResponse r = adminRestSession.get("/projects/" + project.get()
+        + "/branches/master/mergeable?source=branchA");
+    MergeableInfo m = newGson().fromJson(r.getReader(), MergeableInfo.class);
+    assertThat(m.mergeable).isFalse();
+    assertThat(m.conflicts).containsExactly("a.txt");
+  }
+
+  private ChangeInput newMergeChangeInput(String targetBranch,
+      String sourceRef, ChangeStatus status) {
+    // create a merge change from branchA to master in gerrit
+    ChangeInput in = new ChangeInput();
+    in.project = project.get();
+    in.branch = targetBranch;
+    in.subject = "merge " + sourceRef + " to " + targetBranch;
+    in.status = status;
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = sourceRef;
+    in.merge = mergeInput;
+    return in;
+  }
+
+  private void changeInTwoBranches(String fileInMaster, String fileInBranchA)
+      throws Exception {
+    // create a initial commit in master
+    Result initialCommit = pushFactory
+        .create(db, user.getIdent(), testRepo, "initial commit", "readme.txt",
+            "initial commit")
+        .to("refs/heads/master");
+    initialCommit.assertOkStatus();
+
+    // create a new branch branchA
+    createBranch(new Branch.NameKey(project, "branchA"));
+
+    // create another commit in master
+    Result changeToMaster = pushFactory
+        .create(db, user.getIdent(), testRepo, "change to master", fileInMaster,
+            "master content")
+        .to("refs/heads/master");
+    changeToMaster.assertOkStatus();
+
+    // create a commit in branchA
+    PushOneCommit commit = pushFactory
+        .create(db, user.getIdent(), testRepo, "change to branchA",
+            fileInBranchA, "branchA content");
+    commit.setParent(initialCommit.getCommit());
+    commit.to("refs/heads/branchA");
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
index 9f0df93..88c3ea8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -25,4 +25,5 @@
   public ChangeStatus status;
   public String baseChange;
   public Boolean newBranch;
+  public MergeInput merge;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java
new file mode 100644
index 0000000..598d618
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class MergeInput {
+  /**
+   * {@code source} can be any Git object reference expression.
+   *
+   * @see <a href="https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html">gitrevisions(7)</a>
+   */
+  public String source;
+
+  /**
+   * {@code strategy} name of the merge strategy.
+   *
+   * @see org.eclipse.jgit.merge.MergeStrategy
+   */
+  public String strategy;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
index 9c38055..16091a3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
@@ -20,6 +20,8 @@
 
 public class MergeableInfo {
   public SubmitType submitType;
+  public String strategy;
   public boolean mergeable;
+  public List<String> conflicts;
   public List<String> mergeableInto;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 6d8a2e0..8f94c6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -16,6 +16,7 @@
 
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.TimeUtil;
@@ -24,6 +25,7 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -50,10 +52,12 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.RefControl;
@@ -99,6 +103,7 @@
   private final BatchUpdate.Factory updateFactory;
   private final PatchSetUtil psUtil;
   private final boolean allowDrafts;
+  private final MergeUtil.Factory mergeUtilFactory;
 
   @Inject
   CreateChange(@AnonymousCowardName String anonymousCowardName,
@@ -114,7 +119,8 @@
       ChangeFinder changeFinder,
       BatchUpdate.Factory updateFactory,
       PatchSetUtil psUtil,
-      @GerritServerConfig Config config) {
+      @GerritServerConfig Config config,
+      MergeUtil.Factory mergeUtilFactory) {
     this.anonymousCowardName = anonymousCowardName;
     this.db = db;
     this.gitManager = gitManager;
@@ -129,6 +135,7 @@
     this.updateFactory = updateFactory;
     this.psUtil = psUtil;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
+    this.mergeUtilFactory = mergeUtilFactory;
   }
 
   @Override
@@ -173,7 +180,8 @@
 
     Project.NameKey project = rsrc.getNameKey();
     try (Repository git = gitManager.openRepository(project);
-        RevWalk rw = new RevWalk(git)) {
+         ObjectInserter oi = git.newObjectInserter();
+         RevWalk rw = new RevWalk(oi.newReader())) {
       ObjectId parentCommit;
       List<String> groups;
       if (input.baseChange != null) {
@@ -219,42 +227,47 @@
       GeneralPreferencesInfo info =
           account.getAccount().getGeneralPreferencesInfo();
 
-      try (ObjectInserter oi = git.newObjectInserter()) {
-        ObjectId treeId =
-            mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
-        ObjectId id = ChangeIdUtil.computeChangeId(treeId,
-            mergeTip, author, author, input.subject);
-        String commitMessage = ChangeIdUtil.insertId(input.subject, id);
-        if (Boolean.TRUE.equals(info.signedOffBy)) {
-          commitMessage += String.format("%s%s",
-              SIGNED_OFF_BY_TAG,
-              account.getAccount().getNameEmail(anonymousCowardName));
-        }
-
-        RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage);
-
-        Change.Id changeId = new Change.Id(seq.nextChangeId());
-        ChangeInserter ins = changeInserterFactory.create(changeId, c, refName)
-            .setValidatePolicy(CommitValidators.Policy.GERRIT);
-        ins.setMessage(String.format("Uploaded patch set %s.",
-            ins.getPatchSetId().get()));
-        String topic = input.topic;
-        if (topic != null) {
-          topic = Strings.emptyToNull(topic.trim());
-        }
-        ins.setTopic(topic);
-        ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
-        ins.setGroups(groups);
-        try (BatchUpdate bu = updateFactory.create(
-            db.get(), project, me, now)) {
-          bu.setRepository(git, rw, oi);
-          bu.insertChange(ins);
-          bu.execute();
-        }
-        ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
-        return Response.created(json.format(ins.getChange()));
+      ObjectId treeId =
+          mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
+      ObjectId id = ChangeIdUtil.computeChangeId(treeId,
+          mergeTip, author, author, input.subject);
+      String commitMessage = ChangeIdUtil.insertId(input.subject, id);
+      if (Boolean.TRUE.equals(info.signedOffBy)) {
+        commitMessage += String.format("%s%s",
+            SIGNED_OFF_BY_TAG,
+            account.getAccount().getNameEmail(anonymousCowardName));
       }
 
+      RevCommit c;
+      if (input.merge != null) {
+        // create a merge commit
+        c = newMergeCommit(git, oi, rw, rsrc.getControl(), mergeTip, input.merge,
+            author, commitMessage);
+      } else {
+        // create an empty commit
+        c = newCommit(oi, rw, author, mergeTip, commitMessage);
+      }
+
+      Change.Id changeId = new Change.Id(seq.nextChangeId());
+      ChangeInserter ins = changeInserterFactory.create(changeId, c, refName)
+          .setValidatePolicy(CommitValidators.Policy.GERRIT);
+      ins.setMessage(String.format("Uploaded patch set %s.",
+          ins.getPatchSetId().get()));
+      String topic = input.topic;
+      if (topic != null) {
+        topic = Strings.emptyToNull(topic.trim());
+      }
+      ins.setTopic(topic);
+      ins.setDraft(input.status == ChangeStatus.DRAFT);
+      ins.setGroups(groups);
+      try (BatchUpdate bu = updateFactory.create(
+          db.get(), project, me, now)) {
+        bu.setRepository(git, rw, oi);
+        bu.insertChange(ins);
+        bu.execute();
+      }
+      ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
+      return Response.created(json.format(ins.getChange()));
     }
   }
 
@@ -274,6 +287,32 @@
     return rw.parseCommit(insert(oi, commit));
   }
 
+  private RevCommit newMergeCommit(Repository repo, ObjectInserter oi,
+      RevWalk rw, ProjectControl projectControl, RevCommit mergeTip,
+      MergeInput merge, PersonIdent authorIdent, String commitMessage)
+      throws RestApiException, IOException {
+    if (Strings.isNullOrEmpty(merge.source)) {
+      throw new BadRequestException("merge.source must be non-empty");
+    }
+
+    RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source);
+    if (!projectControl.canReadCommit(db.get(), repo, sourceCommit)) {
+      throw new BadRequestException(
+          "do not have read permission for: " + merge.source);
+    }
+
+    MergeUtil mergeUtil =
+        mergeUtilFactory.create(projectControl.getProjectState());
+    // default merge strategy from project settings
+    String mergeStrategy = MoreObjects.firstNonNull(
+        Strings.emptyToNull(merge.strategy),
+        mergeUtil.mergeStrategyName());
+
+    return rw.parseCommit(MergeUtil
+        .createMergeCommit(repo, oi, mergeTip, sourceCommit, mergeStrategy,
+            authorIdent, commitMessage, rw));
+  }
+
   private static ObjectId insert(ObjectInserter inserter,
       CommitBuilder commit) throws IOException,
       UnsupportedEncodingException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index 08ef76e..aacc8aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -41,6 +41,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -110,6 +111,7 @@
       ProjectState projectState = projectCache.get(change.getProject());
       String strategy = mergeUtilFactory.create(projectState)
           .mergeStrategyName();
+      result.strategy = strategy;
       result.mergeable =
           isMergable(git, change, commit, ref, result.submitType, strategy);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
index 109fa76..e560034 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
@@ -14,9 +14,16 @@
 
 package com.google.gerrit.server.git;
 
-/** Indicates that the commit is already contained in destination banch. */
-public class MergeIdenticalTreeException extends Exception {
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/**
+ * Indicates that the commit is already contained in destination branch.
+ *
+ */
+public class MergeIdenticalTreeException extends RestApiException {
   private static final long serialVersionUID = 1L;
+
+  /** @param msg message to return to the client describing the error. */
   public MergeIdenticalTreeException(String msg) {
     super(msg, null);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 7c36961..e30550e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -19,12 +19,15 @@
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -45,11 +48,13 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
+import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -60,6 +65,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ResolveMerger;
 import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.FooterKey;
@@ -202,6 +208,44 @@
     throw new MergeConflictException("merge conflict");
   }
 
+  public static ObjectId createMergeCommit(Repository repo, ObjectInserter inserter,
+      RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy,
+      PersonIdent committerIndent, String commitMsg, RevWalk rw)
+      throws IOException, MergeIdenticalTreeException, MergeConflictException {
+
+    if (rw.isMergedInto(originalCommit, mergeTip)) {
+      throw new MergeIdenticalTreeException(
+          "merge identical tree: change(s) has been already merged!");
+    }
+
+    Merger m = newMerger(repo, inserter, mergeStrategy);
+    if (m.merge(false, mergeTip, originalCommit)) {
+      ObjectId tree = m.getResultTreeId();
+
+      CommitBuilder mergeCommit = new CommitBuilder();
+      mergeCommit.setTreeId(tree);
+      mergeCommit.setParentIds(mergeTip, originalCommit);
+      mergeCommit.setAuthor(committerIndent);
+      mergeCommit.setCommitter(committerIndent);
+      mergeCommit.setMessage(commitMsg);
+      return inserter.insert(mergeCommit);
+    } else {
+      List<String> conflicts = ImmutableList.of();
+      if (m instanceof ResolveMerger) {
+        conflicts = ((ResolveMerger) m).getUnmergedPaths();
+      }
+      throw new MergeConflictException(createConflictMessage(conflicts));
+    }
+  }
+
+  public static String createConflictMessage(List<String> conflicts) {
+    StringBuilder sb = new StringBuilder("merge conflict(s)");
+    for (String c : conflicts) {
+      sb.append('\n' + c);
+    }
+    return sb.toString();
+  }
+
   public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl,
       PatchSet.Id psId) {
     Change c = ctl.getChange();
@@ -591,11 +635,17 @@
 
   public static ThreeWayMerger newThreeWayMerger(Repository repo,
       final ObjectInserter inserter, String strategyName) {
+    Merger m = newMerger(repo, inserter, strategyName);
+    checkArgument(m instanceof ThreeWayMerger,
+        "merge strategy %s does not support three-way merging", strategyName);
+    return (ThreeWayMerger) m;
+  }
+
+  public static Merger newMerger(Repository repo,
+      final ObjectInserter inserter, String strategyName) {
     MergeStrategy strategy = MergeStrategy.get(strategyName);
     checkArgument(strategy != null, "invalid merge strategy: %s", strategyName);
     Merger m = strategy.newMerger(repo, true);
-    checkArgument(m instanceof ThreeWayMerger,
-        "merge strategy %s does not support three-way merging", strategyName);
     m.setObjectInserter(new ObjectInserter.Filter() {
       @Override
       protected ObjectInserter delegate() {
@@ -610,7 +660,7 @@
       public void close() {
       }
     });
-    return (ThreeWayMerger) m;
+    return m;
   }
 
   public void markCleanMerges(final RevWalk rw,
@@ -693,4 +743,16 @@
     }
     return null;
   }
+
+  public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
+      throws BadRequestException, ResourceNotFoundException, IOException {
+    try {
+      return rw.parseCommit(repo.resolve(str));
+    } catch (AmbiguousObjectException | IncorrectObjectTypeException |
+        RevisionSyntaxException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (MissingObjectException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
new file mode 100644
index 0000000..fd4e359
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+
+/**
+ * Check the mergeability at current branch for a git object references expression.
+ */
+public class CheckMergeability implements RestReadView<BranchResource> {
+
+  private String source;
+  private String strategy;
+  private final Provider<ReviewDb> db;
+
+  @Option(name = "--source", metaVar = "COMMIT",
+      usage = "the source reference to merge, which could be any git object "
+          + "references expression, refer to "
+          + "org.eclipse.jgit.lib.Repository#resolve(String)",
+      required = true)
+  public void setSource(String source) {
+    this.source = source;
+  }
+
+  @Option(name = "--strategy", metaVar = "STRATEGY",
+      usage = "name of the merge strategy, refer to "
+          + "org.eclipse.jgit.merge.MergeStrategy")
+  public void setStrategy(String strategy) {
+    this.strategy = strategy;
+  }
+
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  CheckMergeability(GitRepositoryManager gitManager,
+      @GerritServerConfig Config cfg,
+      Provider<ReviewDb> db) {
+    this.gitManager = gitManager;
+    this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
+    this.db = db;
+  }
+
+  @Override
+  public MergeableInfo apply(BranchResource resource)
+      throws IOException, BadRequestException, ResourceNotFoundException {
+    MergeableInfo result = new MergeableInfo();
+    result.strategy = strategy;
+    try (Repository git = gitManager.openRepository(resource.getNameKey());
+         RevWalk rw = new RevWalk(git);
+         ObjectInserter inserter = new InMemoryInserter(git)) {
+      Merger m = MergeUtil.newMerger(git, inserter, strategy);
+
+      Ref destRef = git.getRefDatabase().exactRef(resource.getRef());
+      if (destRef == null) {
+        throw new ResourceNotFoundException(resource.getRef());
+      }
+
+      RevCommit targetCommit = rw.parseCommit(destRef.getObjectId());
+      RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, source);
+
+      if (!resource.getControl().canReadCommit(db.get(), git, sourceCommit)) {
+        throw new BadRequestException(
+            "do not have read permission for: " + source);
+      }
+
+      result.mergeable = m.merge(targetCommit, sourceCommit);
+      if (m instanceof ResolveMerger) {
+        result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
+      }
+    }
+    return result;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 341d741..9706298 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -68,6 +68,7 @@
     delete(BRANCH_KIND).to(DeleteBranch.class);
     post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
     factory(CreateBranch.Factory.class);
+    get(BRANCH_KIND, "mergeable").to(CheckMergeability.class);
     get(BRANCH_KIND, "reflog").to(GetReflog.class);
     child(BRANCH_KIND, "files").to(FilesCollection.class);
     get(FILE_KIND, "content").to(GetContent.class);