Merge "Fix gr-registration-dialog test failure in Safari"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index d8214c2..236d30e 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -517,6 +517,61 @@
   }
 ----
 
+[[create-merge-patch-set-for-change]]
+=== Create Merge Patch Set For Change
+--
+'POST /changes/link:#change-id[\{change-id\}]/merge'
+--
+
+Update an existing change by using a
+link:#merge-patch-set-input[MergePatchSetInput] entity.
+
+Gerrit will create a merge commit based on the information of
+MergePatchSetInput and add a new patch set to the change corresponding
+to the new merge commit.
+
+.Request
+----
+  POST /changes/test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc/merge  HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "source": "refs/12/1234/1"
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity with current revision is
+returned that describes the resulting change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc",
+    "project": "test",
+    "branch": "master",
+    "hashtags": [],
+    "change_id": "Ic5466d107c5294414710935a8ef3b0180fb848dc",
+    "subject": "Merge dev_branch into master",
+    "status": "NEW",
+    "created": "2016-09-23 18:08:53.238000000",
+    "updated": "2016-09-23 18:09:25.934000000",
+    "submit_type": "MERGE_IF_NECESSARY",
+    "mergeable": true,
+    "insertions": 5,
+    "deletions": 0,
+    "_number": 72,
+    "owner": {
+      "_account_id": 1000000
+    },
+    "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822"
+  }
+----
+
 [[get-change-detail]]
 === Get Change Detail
 --
@@ -5367,6 +5422,25 @@
 `simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
 |============================
 
+[[merge-patch-set-input]]
+=== MergePatchSetInput
+The `MergePatchSetInput` entity contains information about updating a new
+change by creating a new merge commit.
+
+[options="header",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`subject`            |optional|
+The new subject for the change, if not specified, will reuse the current patch
+set's subject
+|`inheritParent`      |optional, default to `false`|
+Use the current patch set's first parent as the merge tip when set to `true`.
+Otherwise, use the current branch tip of the destination branch.
+|`merge`              ||
+The detail of the source commit for merge as a link:#merge-input[MergeInput]
+entity.
+|==================================
+
 [[move-input]]
 === MoveInput
 The `MoveInput` entity contains information for moving a change to a new branch.
diff --git a/WORKSPACE b/WORKSPACE
index 4487821..e356c40 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -309,8 +309,8 @@
 
 maven_jar(
   name = 'commons_compress',
-  artifact = 'org.apache.commons:commons-compress:1.7',
-  sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
+  artifact = 'org.apache.commons:commons-compress:1.12',
+  sha1 = '84caa68576e345eb5e7ae61a0e5a9229eb100d7b',
 )
 
 maven_jar(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index dc829dc..7c6ce0e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -61,9 +61,11 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -71,6 +73,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -1921,6 +1924,86 @@
         + r1.getChange().getId().id + ".");
   }
 
+  @Test
+  public void testCreateMergePatchSet() throws Exception {
+    PushOneCommit.Result start = pushTo("refs/heads/master");
+    start.assertOkStatus();
+    // create a change for master
+    PushOneCommit.Result r = createChange();
+    r.assertOkStatus();
+    String changeId = r.getChangeId();
+
+    testRepo.reset(start.getCommit());
+    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
+    currentMaster.assertOkStatus();
+    String parent = currentMaster.getCommit().getName();
+
+    // push a commit into dev branch
+    createBranch(new Branch.NameKey(project, "dev"));
+    PushOneCommit.Result changeA = pushFactory
+        .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
+        .to("refs/heads/dev");
+    changeA.assertOkStatus();
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = "dev";
+    MergePatchSetInput in = new MergePatchSetInput();
+    in.merge = mergeInput;
+    in.subject = "update change by merge ps2";
+    gApi.changes().id(changeId).createMergePatchSet(in);
+    ChangeInfo changeInfo = gApi.changes().id(changeId)
+        .get(EnumSet.of(ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.CURRENT_COMMIT,
+            ListChangesOption.CURRENT_REVISION));
+    assertThat(changeInfo.revisions.size()).isEqualTo(2);
+    assertThat(changeInfo.subject).isEqualTo(in.subject);
+    assertThat(
+        changeInfo.revisions.get(changeInfo.currentRevision).commit.parents
+            .get(0).commit).isEqualTo(parent);
+  }
+
+  @Test
+  public void testCreateMergePatchSetInheritParent() throws Exception {
+    PushOneCommit.Result start = pushTo("refs/heads/master");
+    start.assertOkStatus();
+    // create a change for master
+    PushOneCommit.Result r = createChange();
+    r.assertOkStatus();
+    String changeId = r.getChangeId();
+    String parent = r.getCommit().getParent(0).getName();
+
+    // advance master branch
+    testRepo.reset(start.getCommit());
+    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
+    currentMaster.assertOkStatus();
+
+    // push a commit into dev branch
+    createBranch(new Branch.NameKey(project, "dev"));
+    PushOneCommit.Result changeA = pushFactory
+        .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
+        .to("refs/heads/dev");
+    changeA.assertOkStatus();
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = "dev";
+    MergePatchSetInput in = new MergePatchSetInput();
+    in.merge = mergeInput;
+    in.subject = "update change by merge ps2 inherit parent of ps1";
+    in.inheritParent = true;
+    gApi.changes().id(changeId).createMergePatchSet(in);
+    ChangeInfo changeInfo = gApi.changes().id(changeId)
+        .get(EnumSet.of(ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.CURRENT_COMMIT,
+            ListChangesOption.CURRENT_REVISION));
+
+    assertThat(changeInfo.revisions.size()).isEqualTo(2);
+    assertThat(changeInfo.subject).isEqualTo(in.subject);
+    assertThat(
+        changeInfo.revisions.get(changeInfo.currentRevision).commit.parents
+            .get(0).commit).isEqualTo(parent);
+    assertThat(
+        changeInfo.revisions.get(changeInfo.currentRevision).commit.parents
+            .get(0).commit).isNotEqualTo(currentMaster.getCommit().getName());
+  }
+
   private static Iterable<Account.Id> getReviewers(
       Collection<AccountInfo> r) {
     return Iterables.transform(r, a -> new Account.Id(a._accountId));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 6e2e8b1..1e2fed5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -652,6 +652,58 @@
     assertTwoChangesWithSameRevision(r);
   }
 
+  @Test
+  public void pushSameCommitTwice() throws Exception {
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject()
+        .setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
+
+    PushOneCommit push =
+        pushFactory
+            .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+                "a.txt", "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    push =
+        pushFactory
+            .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+                "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    assertPushRejected(pushHead(testRepo, "refs/for/master", false),
+        "refs/for/master", "commit(s) already exists (as current patchset)");
+  }
+
+  @Test
+  public void pushSameCommitTwiceWhenIndexFailed() throws Exception {
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject()
+        .setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
+
+    PushOneCommit push =
+        pushFactory
+            .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+                "a.txt", "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    push =
+        pushFactory
+            .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+                "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    indexer.delete(r.getChange().getId());
+
+    assertPushRejected(pushHead(testRepo, "refs/for/master", false),
+        "refs/for/master", "commit(s) already exists (as current patchset)");
+  }
+
   private void assertTwoChangesWithSameRevision(PushOneCommit.Result result)
       throws Exception {
     List<ChangeInfo> changes = query(result.getCommit().name());
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index d9cd562..8e36a77 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -95,6 +96,9 @@
    */
   ChangeApi revert(RevertInput in) throws RestApiException;
 
+  /** Create a merge patch set for the change. */
+  ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
+
   List<ChangeInfo> submittedTogether() throws RestApiException;
   SubmittedTogetherInfo submittedTogether(
       EnumSet<SubmittedTogetherOption> options) throws RestApiException;
@@ -412,5 +416,11 @@
         EnumSet<SubmittedTogetherOption> b) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public ChangeInfo createMergePatchSet(MergePatchSetInput in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java
new file mode 100644
index 0000000..263b6c4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java
@@ -0,0 +1,21 @@
+// 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 MergePatchSetInput {
+  public String subject;
+  public boolean inheritParent;
+  public MergeInput merge;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index f7cb8f4..7532a11 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -63,7 +64,9 @@
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.change.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestChangeReviewers;
+import com.google.gerrit.server.change.CreateMergePatchSet;
 import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -91,6 +94,7 @@
   private final Abandon abandon;
   private final Revert revert;
   private final Restore restore;
+  private final CreateMergePatchSet updateByMerge;
   private final Provider<SubmittedTogether> submittedTogether;
   private final PublishDraftPatchSet.CurrentRevision
     publishDraftChange;
@@ -122,6 +126,7 @@
       Abandon abandon,
       Revert revert,
       Restore restore,
+      CreateMergePatchSet updateByMerge,
       Provider<SubmittedTogether> submittedTogether,
       PublishDraftPatchSet.CurrentRevision publishDraftChange,
       DeleteDraftChange deleteDraftChange,
@@ -151,6 +156,7 @@
     this.suggestReviewers = suggestReviewers;
     this.abandon = abandon;
     this.restore = restore;
+    this.updateByMerge = updateByMerge;
     this.submittedTogether = submittedTogether;
     this.publishDraftChange = publishDraftChange;
     this.deleteDraftChange = deleteDraftChange;
@@ -268,6 +274,17 @@
   }
 
   @Override
+  public ChangeInfo createMergePatchSet(MergePatchSetInput in)
+      throws RestApiException {
+    try {
+      return updateByMerge.apply(change, in).value();
+    } catch (IOException | UpdateException | InvalidChangeOperationException
+        | NoSuchChangeException | OrmException e) {
+      throw new RestApiException("Cannot update change by merge", e);
+    }
+  }
+
+  @Override
   public List<ChangeInfo> submittedTogether() throws RestApiException {
     SubmittedTogetherInfo info = submittedTogether(
         EnumSet.noneOf(ListChangesOption.class),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
new file mode 100644
index 0000000..f117179
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -0,0 +1,212 @@
+// 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.change;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
+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.MergeConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeIdenticalTreeException;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.ChangeIdUtil;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.EnumSet;
+import java.util.TimeZone;
+
+@Singleton
+public class CreateMergePatchSet implements
+    RestModifyView<ChangeResource, MergePatchSetInput> {
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager gitManager;
+  private final TimeZone serverTimeZone;
+  private final Provider<CurrentUser> user;
+  private final ChangeJson.Factory jsonFactory;
+  private final PatchSetUtil psUtil;
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+
+  @Inject
+  CreateMergePatchSet(Provider<ReviewDb> db,
+      GitRepositoryManager gitManager,
+      @GerritPersonIdent PersonIdent myIdent,
+      Provider<CurrentUser> user,
+      ChangeJson.Factory json,
+      PatchSetUtil psUtil,
+      MergeUtil.Factory mergeUtilFactory,
+      BatchUpdate.Factory batchUpdateFactory,
+      PatchSetInserter.Factory patchSetInserterFactory) {
+    this.db = db;
+    this.gitManager = gitManager;
+    this.serverTimeZone = myIdent.getTimeZone();
+    this.user = user;
+    this.jsonFactory = json;
+    this.psUtil = psUtil;
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource req, MergePatchSetInput in)
+      throws NoSuchChangeException, OrmException, IOException,
+      InvalidChangeOperationException, RestApiException, UpdateException {
+    if (in.merge == null) {
+      throw new BadRequestException("merge field is required");
+    }
+
+    MergeInput merge = in.merge;
+    if (Strings.isNullOrEmpty(merge.source)) {
+      throw new BadRequestException("merge.source must be non-empty");
+    }
+
+    ChangeControl ctl = req.getControl();
+    if (!ctl.isVisible(db.get())) {
+      throw new InvalidChangeOperationException(
+          "Base change not found: " + req.getId());
+    }
+    PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
+    if (!ctl.canAddPatchSet(db.get())) {
+      throw new AuthException("cannot add patch set");
+    }
+
+    ProjectControl projectControl = ctl.getProjectControl();
+    Change change = ctl.getChange();
+    Project.NameKey project = change.getProject();
+    Branch.NameKey dest = change.getDest();
+    try (Repository git = gitManager.openRepository(project);
+        ObjectInserter oi = git.newObjectInserter();
+        RevWalk rw = new RevWalk(oi.newReader())) {
+
+      RevCommit sourceCommit =
+          MergeUtil.resolveCommit(git, rw, merge.source);
+      if (!projectControl.canReadCommit(db.get(), git, sourceCommit)) {
+        throw new ResourceNotFoundException(
+            "cannot find source commit: " + merge.source + " to merge.");
+      }
+
+      RevCommit currentPsCommit =
+          rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+
+      Timestamp now = TimeUtil.nowTs();
+      IdentifiedUser me = user.get().asIdentifiedUser();
+      PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
+
+      RevCommit newCommit =
+          createMergeCommit(in, projectControl, dest, git, oi, rw,
+              currentPsCommit, sourceCommit, author,
+              ObjectId.fromString(change.getKey().get().substring(1)));
+
+      PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
+      PatchSetInserter psInserter =
+          patchSetInserterFactory.create(ctl, nextPsId, newCommit);
+      try (BatchUpdate bu = batchUpdateFactory
+          .create(db.get(), project, me, now)) {
+        bu.setRepository(git, rw, oi);
+        bu.addOp(ctl.getId(), psInserter
+            .setMessage("Uploaded patch set " + nextPsId.get() + ".")
+            .setDraft(ps.isDraft())
+            .setNotify(NotifyHandling.NONE));
+        bu.execute();
+      }
+
+      ChangeJson json =
+          jsonFactory.create(EnumSet.of(ListChangesOption.CURRENT_REVISION));
+      return Response.ok(json.format(psInserter.getChange()));
+    }
+  }
+
+  private RevCommit createMergeCommit(MergePatchSetInput in,
+      ProjectControl projectControl, Branch.NameKey dest, Repository git,
+      ObjectInserter oi, RevWalk rw, RevCommit currentPsCommit,
+      RevCommit sourceCommit, PersonIdent author, ObjectId changeId)
+      throws ResourceNotFoundException, MergeIdenticalTreeException,
+      MergeConflictException, IOException {
+
+    ObjectId parentCommit;
+    if (in.inheritParent) {
+      // inherit first parent from previous patch set
+      parentCommit = currentPsCommit.getParent(0);
+    } else {
+      // get the current branch tip of destination branch
+      Ref destRef = git.getRefDatabase().exactRef(dest.get());
+      if (destRef != null) {
+        parentCommit = destRef.getObjectId();
+      } else {
+        throw new ResourceNotFoundException("cannot find destination branch");
+      }
+    }
+    RevCommit mergeTip = rw.parseCommit(parentCommit);
+
+    String commitMsg;
+    if (Strings.emptyToNull(in.subject) != null) {
+      commitMsg = ChangeIdUtil.insertId(in.subject, changeId);
+    } else {
+      // reuse previous patch set commit message
+      commitMsg = currentPsCommit.getFullMessage();
+    }
+
+    String mergeStrategy = MoreObjects.firstNonNull(
+        Strings.emptyToNull(in.merge.strategy),
+        mergeUtilFactory.create(projectControl.getProjectState())
+            .mergeStrategyName());
+
+    return MergeUtil.createMergeCommit(git, oi, mergeTip, sourceCommit,
+        mergeStrategy, author, commitMsg, rw);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index a52920a..bb76084 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -53,6 +53,7 @@
     DynamicMap.mapOf(binder(), VOTE_KIND);
 
     get(CHANGE_KIND).to(GetChange.class);
+    post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class);
     get(CHANGE_KIND, "detail").to(GetDetail.class);
     get(CHANGE_KIND, "topic").to(GetTopic.class);
     get(CHANGE_KIND, "in").to(IncludedIn.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 49221c9..7457da5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -664,11 +664,9 @@
         op.updateRepo(ctx);
       }
 
-      if (!repoOnlyOps.isEmpty()) {
-        logDebug("Executing updateRepo on {} RepoOnlyOps", ops.size());
-        for (RepoOnlyOp op : repoOnlyOps) {
-          op.updateRepo(ctx);
-        }
+      logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size());
+      for (RepoOnlyOp op : repoOnlyOps) {
+        op.updateRepo(ctx);
       }
 
       if (inserter != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index a23b78c..74362b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -101,6 +101,7 @@
 import com.google.gerrit.server.git.validators.RefOperationValidationException;
 import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -333,6 +334,7 @@
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
+  private final ChangeIndexer indexer;
 
   private final List<ValidationMessage> messages = new ArrayList<>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -377,6 +379,7 @@
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       NotesMigration notesMigration,
       ChangeEditUtil editUtil,
+      ChangeIndexer indexer,
       BatchUpdate.Factory batchUpdateFactory,
       SetHashtagsOp.Factory hashtagsFactory,
       ReplaceOp.Factory replaceOpFactory,
@@ -424,6 +427,7 @@
     this.notesMigration = notesMigration;
 
     this.editUtil = editUtil;
+    this.indexer = indexer;
 
     this.messageSender = new ReceivePackMessageSender();
 
@@ -1826,6 +1830,18 @@
             return;
           }
 
+          // In case the change look up from the index failed,
+          // double check against the existing refs
+          if (foundInExistingRef(existing.get(p.commit))) {
+            if (pending.size() == 1) {
+              reject(magicBranch.cmd,
+                  "commit(s) already exists (as current patchset)");
+              newChanges = Collections.emptyList();
+              return;
+            }
+            itr.remove();
+            continue;
+          }
           newChangeIds.add(p.changeKey);
         }
         newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get()));
@@ -1879,6 +1895,22 @@
     }
   }
 
+  private boolean foundInExistingRef(Collection<Ref> existingRefs)
+      throws OrmException {
+    for (Ref ref : existingRefs) {
+      ChangeNotes notes = notesFactory.create(db, project.getNameKey(),
+          Change.Id.fromRef(ref.getName()));
+      Change change = notes.getChange();
+      if (change.getDest().equals(magicBranch.dest)) {
+        logDebug("Found change {} from existing refs.", change.getKey());
+        // reindex the change asynchronously
+        indexer.indexAsync(project.getNameKey(), change.getId());
+        return true;
+      }
+    }
+    return false;
+  }
+
   private RevCommit setUpWalkForSelectingChanges() throws IOException {
     RevWalk rw = rp.getRevWalk();
     RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
@@ -2400,10 +2432,12 @@
       rw.parseBody(newCommit);
 
       RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-      replaceOp = replaceOpFactory.create(requestScopePropagator,
-          projectControl, notes.getChange().getDest(), checkMergedInto,
-          priorPatchSet, priorCommit, psId, newCommit, info, groups,
-          magicBranch, rp.getPushCertificate());
+      replaceOp = replaceOpFactory
+          .create(projectControl, notes.getChange().getDest(), checkMergedInto,
+              priorPatchSet, priorCommit, psId, newCommit, info, groups,
+              magicBranch, rp.getPushCertificate())
+          .setRequestScopePropagator(requestScopePropagator)
+          .setUpdateRef(false);
       bu.addOp(notes.getChangeId(), replaceOp);
       if (progress != null) {
         bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index a9dcd4b..aece8a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -64,6 +64,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -78,7 +79,6 @@
 public class ReplaceOp extends BatchUpdate.Op {
   public interface Factory {
     ReplaceOp create(
-        RequestScopePropagator requestScopePropagator,
         ProjectControl projectControl,
         Branch.NameKey dest,
         boolean checkMergedInto,
@@ -112,7 +112,6 @@
   private final PatchSetUtil psUtil;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
 
-  private final RequestScopePropagator requestScopePropagator;
   private final ProjectControl projectControl;
   private final Branch.NameKey dest;
   private final boolean checkMergedInto;
@@ -133,6 +132,8 @@
   private ChangeMessage msg;
   private String rejectMessage;
   private MergedByPushOp mergedByPushOp;
+  private RequestScopePropagator requestScopePropagator;
+  private boolean updateRef;
 
   @AssistedInject
   ReplaceOp(AccountResolver accountResolver,
@@ -149,7 +150,6 @@
       PatchSetUtil psUtil,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       @SendEmailExecutor ExecutorService sendEmailExecutor,
-      @Assisted RequestScopePropagator requestScopePropagator,
       @Assisted ProjectControl projectControl,
       @Assisted Branch.NameKey dest,
       @Assisted boolean checkMergedInto,
@@ -176,7 +176,6 @@
     this.replacePatchSetFactory = replacePatchSetFactory;
     this.sendEmailExecutor = sendEmailExecutor;
 
-    this.requestScopePropagator = requestScopePropagator;
     this.projectControl = projectControl;
     this.dest = dest;
     this.checkMergedInto = checkMergedInto;
@@ -188,6 +187,7 @@
     this.groups = groups;
     this.magicBranch = magicBranch;
     this.pushCertificate = pushCertificate;
+    this.updateRef = true;
   }
 
   @Override
@@ -203,6 +203,12 @@
             requestScopePropagator, patchSetId, mergedInto.getName());
       }
     }
+
+    if (updateRef) {
+      ctx.addRefUpdate(
+          new ReceiveCommand(ObjectId.zeroId(), commit,
+              patchSetId.toRefName()));
+    }
   }
 
   @Override
@@ -366,8 +372,10 @@
     // BatchUpdate's perspective there is no ref update. Thus we have to fire it
     // manually.
     final Account account = ctx.getAccount();
-    gitRefUpdated.fire(ctx.getProject(), newPatchSet.getRefName(),
-        ObjectId.zeroId(), commit, account);
+    if (!updateRef) {
+      gitRefUpdated.fire(ctx.getProject(), newPatchSet.getRefName(),
+          ObjectId.zeroId(), commit, account);
+    }
 
     if (changeKind != ChangeKind.TRIVIAL_REBASE) {
       Runnable sender = new Runnable() {
@@ -454,10 +462,25 @@
     return newPatchSet;
   }
 
+  public Change getChange() {
+    return change;
+  }
+
   public String getRejectMessage() {
     return rejectMessage;
   }
 
+  public ReplaceOp setUpdateRef(boolean updateRef) {
+    this.updateRef = updateRef;
+    return this;
+  }
+
+  public ReplaceOp setRequestScopePropagator(
+      RequestScopePropagator requestScopePropagator) {
+    this.requestScopePropagator = requestScopePropagator;
+    return this;
+  }
+
   private Ref findMergedInto(Context ctx, String first, RevCommit commit) {
     try {
       RefDatabase refDatabase = ctx.getRepository().getRefDatabase();
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 55c07a6..5c2e9b2 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -19,8 +19,8 @@
 
 maven_jar(
   name = 'compress',
-  id = 'org.apache.commons:commons-compress:1.7',
-  sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
+  id = 'org.apache.commons:commons-compress:1.12',
+  sha1 = '84caa68576e345eb5e7ae61a0e5a9229eb100d7b',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
 )
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 3702c84..c910d8f 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -22,6 +22,12 @@
     properties: {
       hasTooltip: Boolean,
 
+      _isTouchDevice: {
+        type: Boolean,
+        value: function() {
+          return 'ontouchstart' in document.documentElement;
+        },
+      },
       _tooltip: Element,
       _titleText: String,
     },
@@ -29,10 +35,10 @@
     attached: function() {
       if (!this.hasTooltip) { return; }
 
-      this.addEventListener('mouseover', this._handleShowTooltip.bind(this));
-      this.addEventListener('mouseout', this._handleHideTooltip.bind(this));
-      this.addEventListener('focusin', this._handleShowTooltip.bind(this));
-      this.addEventListener('focusout', this._handleHideTooltip.bind(this));
+      this.addEventListener('mouseenter', this._handleShowTooltip.bind(this));
+      this.addEventListener('mouseleave', this._handleHideTooltip.bind(this));
+      this.addEventListener('tap', this._handleHideTooltip.bind(this));
+
       this.listen(window, 'scroll', '_handleWindowScroll');
     },
 
@@ -41,6 +47,8 @@
     },
 
     _handleShowTooltip: function(e) {
+      if (this._isTouchDevice) { return; }
+
       if (!this.hasAttribute('title') ||
           this.getAttribute('title') === '' ||
           this._tooltip) {
@@ -66,9 +74,11 @@
     },
 
     _handleHideTooltip: function(e) {
+      if (this._isTouchDevice) { return; }
       if (!this.hasAttribute('title') ||
-          this._titleText == null ||
-          this === document.activeElement) { return; }
+          this._titleText == null) {
+        return;
+      }
 
       this.setAttribute('title', this._titleText);
       if (this._tooltip && this._tooltip.parentNode) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index f15b554..6e2c058 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -430,5 +430,45 @@
         MockInteractions.tap(element.$$('.send'));
       });
     });
+
+    test('don"t display tooltips on touch devices', function() {
+      element.labels = {
+        Verified: {
+          values: {
+            '-1': 'Fails',
+            ' 0': 'No score',
+            '+1': 'Verified'
+          },
+          default_value: 0
+        },
+        'Code-Review': {
+          values: {
+            '-2': 'Do not submit',
+            '-1': 'I would prefer that you didn\'t submit this',
+            ' 0': 'No score',
+            '+1': 'Looks good to me, but someone else must approve',
+            '+2': 'Looks good to me, approved'
+          },
+          default_value: 0
+        }
+      };
+      var verifiedBtn = element.$$(
+          'iron-selector[data-label="Verified"] > ' +
+          'gr-button[data-value="-1"]');
+
+      // On touch devices, tooltips should not be shown
+      verifiedBtn._isTouchDevice = true;
+      verifiedBtn._handleShowTooltip();
+      assert.isNotOk(verifiedBtn._tooltip);
+      verifiedBtn._handleHideTooltip();
+      assert.isNotOk(verifiedBtn._tooltip);
+
+      // On other devices, tooltips should be shown.
+      verifiedBtn._isTouchDevice = false;
+      verifiedBtn._handleShowTooltip();
+      assert.isOk(verifiedBtn._tooltip);
+      verifiedBtn._handleHideTooltip();
+      assert.isNotOk(verifiedBtn._tooltip);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 685f568..431c795 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -266,11 +266,14 @@
       assert.isTrue(commitHandler.called);
     });
 
-    test('_focused flag properly triggered', function() {
-      flushAsynchronousOperations();
-      assert.isFalse(element._focused);
-      element.$.input.focus();
-      assert.isTrue(element._focused);
+    test('_focused flag properly triggered', function(done) {
+      flush(function() {
+        assert.isFalse(element._focused);
+        var input = element.$$('input');
+        MockInteractions.focus(input);
+        assert.isTrue(element._focused);
+        done();
+      });
     });
 
     test('_focused flag shows/hides the suggestions', function() {