Merge changes Ie3bd69af,Ia74b3013

* changes:
  PolyGerrit: explain how to run a single test file.
  Add support for running (single) tests with 'run-server.sh'
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4fe2033..51ba60f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -983,6 +983,84 @@
   The change could not be rebased due to a path conflict during merge.
 ----
 
+[[move-change]]
+=== Move Change
+--
+'POST /changes/link:#change-id[\{change-id\}]/move'
+--
+
+Move a change.
+
+The destination branch must be provided in the request body inside a
+link:#move-input[MoveInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/move HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "destination_branch" : "release-branch"
+  }
+
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the moved change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~release-branch~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "release-branch",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 2,
+    "deletions": 13,
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be moved because the change state doesn't
+allow moving the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=UTF-8
+
+  change is merged
+----
+
+If the change cannot be moved because the user doesn't have
+abandon permission on the change or upload permission on the destination,
+the response is "`409 Conflict`" and the error message is contained in the
+response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=UTF-8
+
+  move not permitted
+----
+
 [[revert-change]]
 === Revert Change
 --
@@ -4495,6 +4573,18 @@
 A list of other branch names where this change could merge cleanly
 |============================
 
+[[move-input]]
+=== MoveInput
+The `MoveInput` entity contains information for moving a change to a new branch.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name   ||Description
+|`destination`||Destination branch
+|`message`    |optional|
+A message to be posted in this change's comments
+|===========================
+
 [[problem-info]]
 === ProblemInfo
 The `ProblemInfo` entity contains a description of a potential consistency problem
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 67116cd..9cf1515 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -31,6 +31,8 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -40,6 +42,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -459,6 +462,13 @@
     return pushTo("refs/drafts/master");
   }
 
+  protected BranchApi createBranch(Branch.NameKey branch) throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get())
+        .create(new BranchInput());
+  }
+
   private static final List<Character> RANDOM =
       Chars.asList(new char[]{'a','b','c','d','e','f','g','h'});
   protected PushOneCommit.Result amendChange(String changeId)
@@ -475,6 +485,11 @@
     return push.to(ref);
   }
 
+  protected void merge(PushOneCommit.Result r) throws Exception {
+    revision(r).review(ReviewInput.approve());
+    revision(r).submit();
+  }
+
   protected ChangeInfo info(String id)
       throws RestApiException {
     return gApi.changes().id(id).info();
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 291b953..84e7557 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
@@ -77,6 +77,12 @@
         ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
+        @Assisted("changeId") String changeId);
+
+    PushOneCommit create(
+        ReviewDb db,
+        PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content);
@@ -143,6 +149,18 @@
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
+      @Assisted("changeId") String changeId) throws Exception {
+    this(notesFactory, approvalsUtil, queryProvider,
+        db, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT, changeId);
+  }
+
+  @AssistedInject
+  PushOneCommit(ChangeNotes.Factory notesFactory,
+      ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
+      @Assisted ReviewDb db,
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
       @Assisted("content") String content) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index def8317..24cbac4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -716,11 +716,6 @@
     oldETag = checkETag(getRevisionActions, r2, oldETag);
   }
 
-  private void merge(PushOneCommit.Result r) throws Exception {
-    revision(r).review(ReviewInput.approve());
-    revision(r).submit();
-  }
-
   private PushOneCommit.Result updateChange(PushOneCommit.Result r,
       String content) throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
new file mode 100644
index 0000000..df1ebfd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -0,0 +1,276 @@
+// Copyright (C) 2015 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.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.MoveInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+@NoHttpd
+public class MoveChangeIT extends AbstractDaemonTest {
+  @Test
+  public void moveChange_shortRef() throws Exception {
+    // Move change to a different branch using short ref name
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    move(r.getChangeId(), newBranch.getShortName());
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void moveChange_fullRef() throws Exception {
+    // Move change to a different branch using full ref name
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    move(r.getChangeId(), newBranch.get());
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void moveChangeWithMessage() throws Exception {
+    // Provide a message using --message flag
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    String moveMessage = "Moving for the move test";
+    move(r.getChangeId(), newBranch.get(), moveMessage);
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+    StringBuilder expectedMessage = new StringBuilder();
+    expectedMessage.append("Change destination moved from master to moveTest");
+    expectedMessage.append("\n\n");
+    expectedMessage.append(moveMessage);
+    assertThat(r.getChange().messages().get(1).getMessage())
+        .isEqualTo(expectedMessage.toString());
+  }
+
+  @Test
+  public void moveChangeToSameRefAsCurrent() throws Exception {
+    // Move change to the branch same as change's destination
+    PushOneCommit.Result r = createChange();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is already destined for the specified branch");
+    move(r.getChangeId(), r.getChange().change().getDest().get());
+  }
+
+  @Test
+  public void moveChange_sameChangeId() throws Exception {
+    // Move change to a branch with existing change with same change ID
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    int changeNum = r.getChange().change().getChangeId();
+    createChange(newBranch.get(), r.getChangeId());
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Destination " + newBranch.getShortName()
+        + " has a different change with same change key " + r.getChangeId());
+    move(changeNum, newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToNonExistentRef() throws Exception {
+    // Move change to a non-existing branch
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch = new Branch.NameKey(
+        r.getChange().change().getProject(), "does_not_exist");
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Destination " + newBranch.get()
+        + " not found in the project");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveClosedChange() throws Exception {
+    // Move a change which is not open
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    merge(r);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is merged");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveMergeCommitChange() throws Exception {
+    // Move a change which has a merge commit as the current PS
+    // Create a merge commit and push for review
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    TestRepository<?>.CommitBuilder commitBuilder =
+        testRepo.branch("HEAD").commit().insertChangeId();
+    commitBuilder
+      .parent(r1.getCommit())
+      .parent(r2.getCommit())
+      .message("Move change Merge Commit")
+      .author(admin.getIdent())
+      .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+    RevCommit c = commitBuilder.create();
+    pushHead(testRepo, "refs/for/master", false, false);
+
+    // Try to move the merge commit to another branch
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r1.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Merge commit cannot be moved");
+    move(GitUtil.getChangeId(testRepo, c).get(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToBranch_WithoutUploadPerms() throws Exception {
+    // Move change to a destination where user doesn't have upload permissions
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "blocked_branch");
+    createBranch(newBranch);
+    block(Permission.PUSH,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        "refs/for/" + newBranch.get());
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeFromBranch_WithoutAbandonPerms() throws Exception {
+    // Move change for which user does not have abandon permissions
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    block(Permission.ABANDON,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        r.getChange().change().getDest().get());
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToBranchThatContainsCurrentCommit() throws Exception {
+    // Move change to a branch for which current PS revision is reachable from
+    // tip
+
+    // Create a change
+    PushOneCommit.Result r = createChange();
+    int changeNum = r.getChange().change().getChangeId();
+
+    // Create a branch with that same commit
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchInput bi = new BranchInput();
+    bi.revision = r.getCommit().name();
+    gApi.projects()
+      .name(newBranch.getParentKey().get())
+      .branch(newBranch.get())
+      .create(bi);
+
+    // Try to move the change to the branch with the same commit
+    exception.expect(ResourceConflictException.class);
+    exception
+        .expectMessage("Current patchset revision is reachable from tip of "
+            + newBranch.get());
+    move(changeNum, newBranch.get());
+  }
+
+  @Test
+  public void moveChange_WithCurrentPatchSetLocked() throws Exception {
+    // Move change that is locked
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType patchSetLock = Util.patchSetLock();
+    cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, registeredUsers,
+        "refs/heads/*");
+    saveProjectConfig(cfg);
+    grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
+    revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
+
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      cfg.commit(md);
+    }
+  }
+
+  private void move(int changeNum, String destination)
+      throws RestApiException {
+    gApi.changes().id(changeNum).move(destination);
+  }
+
+  private void move(String changeId, String destination)
+      throws RestApiException {
+    gApi.changes().id(changeId).move(destination);
+  }
+
+  private void move(String changeId, String destination, String message)
+      throws RestApiException {
+    MoveInput in = new MoveInput();
+    in.destination_branch = destination;
+    in.message = message;
+    gApi.changes().id(changeId).move(in);
+  }
+
+  private PushOneCommit.Result createChange(String branch, String changeId)
+      throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    PushOneCommit.Result result = push.to("refs/for/" + branch);
+    result.assertOkStatus();
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index 74487ba..a22b09d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -422,9 +422,9 @@
     }
   }
 
-  private void merge(PushOneCommit.Result r) throws Exception {
-    revision(r).review(ReviewInput.approve());
-    revision(r).submit();
+  @Override
+  protected void merge(PushOneCommit.Result r) throws Exception {
+    super.merge(r);
     try (Repository repo = repoManager.openRepository(project)) {
       assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(
           r.getCommit());
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 c7912cb..ae5f0b8 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
@@ -77,6 +77,9 @@
   void restore() throws RestApiException;
   void restore(RestoreInput in) throws RestApiException;
 
+  void move(String destination) throws RestApiException;
+  void move(MoveInput in) throws RestApiException;
+
   /**
    * Create a new change that reverts this change.
    *
@@ -230,6 +233,16 @@
     }
 
     @Override
+    public void move(String destination) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void move(MoveInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeApi revert() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
new file mode 100644
index 0000000..7e4e5e9
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2015 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.api.changes;
+
+public class MoveInput {
+  public String message;
+  public String destination_branch;
+}
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 a9bf220..274826a 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.MoveInput;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewerApi;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.change.GetTopic;
 import com.google.gerrit.server.change.ListChangeComments;
 import com.google.gerrit.server.change.ListChangeDrafts;
+import com.google.gerrit.server.change.Move;
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
@@ -96,6 +98,7 @@
   private final ListChangeDrafts listDrafts;
   private final Check check;
   private final ChangeEdits.Detail editDetail;
+  private final Move move;
 
   @Inject
   ChangeApiImpl(Provider<CurrentUser> user,
@@ -121,6 +124,7 @@
       ListChangeDrafts listDrafts,
       Check check,
       ChangeEdits.Detail editDetail,
+      Move move,
       @Assisted ChangeResource change) {
     this.user = user;
     this.changeApi = changeApi;
@@ -145,6 +149,7 @@
     this.listDrafts = listDrafts;
     this.check = check;
     this.editDetail = editDetail;
+    this.move = move;
     this.change = change;
   }
 
@@ -212,6 +217,22 @@
   }
 
   @Override
+  public void move(String destination) throws RestApiException {
+    MoveInput in = new MoveInput();
+    in.destination_branch = destination;
+    move(in);
+  }
+
+  @Override
+  public void move(MoveInput in) throws RestApiException {
+    try {
+      move.apply(change, in);
+    } catch (OrmException | UpdateException e) {
+      throw new RestApiException("Cannot move change", e);
+    }
+  }
+
+  @Override
   public ChangeApi revert() throws RestApiException {
     return revert(new RevertInput());
   }
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 8c715a1..4c9c0bf 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
@@ -71,6 +71,7 @@
     post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
     post(CHANGE_KIND, "index").to(Index.class);
     post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class);
+    post(CHANGE_KIND, "move").to(Move.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
     get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
new file mode 100644
index 0000000..f9dc0695
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -0,0 +1,197 @@
+// Copyright (C) 2015 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 static com.google.gerrit.server.query.change.ChangeData.asChanges;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.MoveInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+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.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
+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.UpdateException;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+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.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+@Singleton
+public class Move implements RestModifyView<ChangeResource, MoveInput> {
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeJson.Factory json;
+  private final GitRepositoryManager repoManager;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final ChangeMessagesUtil cmUtil;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final PatchSetUtil psUtil;
+
+  @Inject
+  Move(Provider<ReviewDb> dbProvider,
+      ChangeJson.Factory json,
+      GitRepositoryManager repoManager,
+      Provider<InternalChangeQuery> queryProvider,
+      ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory,
+      PatchSetUtil psUtil) {
+    this.dbProvider = dbProvider;
+    this.json = json;
+    this.repoManager = repoManager;
+    this.queryProvider = queryProvider;
+    this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.psUtil = psUtil;
+  }
+
+  @Override
+  public ChangeInfo apply(ChangeResource req, MoveInput input)
+      throws RestApiException, OrmException, UpdateException {
+    ChangeControl control = req.getControl();
+    input.destination_branch = RefNames.fullName(input.destination_branch);
+    if (!control.canMoveTo(input.destination_branch, dbProvider.get())) {
+      throw new AuthException("Move not permitted");
+    }
+
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) {
+      u.addOp(req.getChange().getId(), new Op(control, input));
+      u.execute();
+    }
+
+    return json.create(ChangeJson.NO_OPTIONS).format(req.getChange());
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final MoveInput input;
+    private final IdentifiedUser caller;
+
+    private Change change;
+    private Branch.NameKey newDestKey;
+
+    public Op(ChangeControl ctl, MoveInput input) {
+      this.input = input;
+      this.caller = ctl.getUser().asIdentifiedUser();
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws OrmException,
+        ResourceConflictException, RepositoryNotFoundException, IOException {
+      change = ctx.getChange();
+      if (change.getStatus() != Status.NEW
+          && change.getStatus() != Status.DRAFT) {
+        throw new ResourceConflictException("Change is " + status(change));
+      }
+
+      Project.NameKey projectKey = change.getProject();
+      newDestKey = new Branch.NameKey(projectKey, input.destination_branch);
+      Branch.NameKey changePrevDest = change.getDest();
+      if (changePrevDest.equals(newDestKey)) {
+        throw new ResourceConflictException(
+            "Change is already destined for the specified branch");
+      }
+
+      final PatchSet.Id patchSetId = change.currentPatchSetId();
+      try (Repository repo = repoManager.openRepository(projectKey);
+          RevWalk revWalk = new RevWalk(repo)) {
+        RevCommit currPatchsetRevCommit = revWalk.parseCommit(
+            ObjectId.fromString(psUtil.current(ctx.getDb(), ctx.getNotes())
+                .getRevision().get()));
+        if (currPatchsetRevCommit.getParentCount() > 1) {
+          throw new ResourceConflictException("Merge commit cannot be moved");
+        }
+
+        ObjectId refId = repo.resolve(input.destination_branch);
+        // Check if destination ref exists in project repo
+        if (refId == null) {
+          throw new ResourceConflictException(
+              "Destination " + input.destination_branch + " not found in the project");
+        }
+        RevCommit refCommit = revWalk.parseCommit(refId);
+        if (revWalk.isMergedInto(currPatchsetRevCommit, refCommit)) {
+          throw new ResourceConflictException(
+              "Current patchset revision is reachable from tip of "
+                  + input.destination_branch);
+        }
+      }
+
+      Change.Key changeKey = change.getKey();
+      if (!asChanges(queryProvider.get().byBranchKey(newDestKey, changeKey))
+          .isEmpty()) {
+        throw new ResourceConflictException(
+            "Destination " + newDestKey.getShortName()
+                + " has a different change with same change key " + changeKey);
+      }
+
+      if (!change.currentPatchSetId().equals(patchSetId)) {
+        throw new ResourceConflictException("Patch set is not current");
+      }
+
+      ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+      update.setBranch(newDestKey.get());
+      change.setDest(newDestKey);
+
+      StringBuilder msgBuf = new StringBuilder();
+      msgBuf.append("Change destination moved from ");
+      msgBuf.append(changePrevDest.getShortName());
+      msgBuf.append(" to ");
+      msgBuf.append(newDestKey.getShortName());
+      if (!Strings.isNullOrEmpty(input.message)) {
+        msgBuf.append("\n\n");
+        msgBuf.append(input.message);
+      }
+      ChangeMessage cmsg = new ChangeMessage(
+          new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+          caller.getAccountId(), ctx.getWhen(), change.currentPatchSetId());
+      cmsg.setMessage(msgBuf.toString());
+      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+
+      ctx.saveChange();
+      return true;
+    }
+  }
+
+  private static String status(Change change) {
+    return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 3b63f95..4bda245 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -236,6 +236,12 @@
         ) && !isPatchSetLocked(db);
   }
 
+  /** Can this user change the destination branch of this change
+      to the new ref? */
+  public boolean canMoveTo(String ref, ReviewDb db) throws OrmException {
+    return getProjectControl().controlForRef(ref).canUpload() && canAbandon(db);
+  }
+
   /** Can this user publish this draft change or any draft patch set of this change? */
   public boolean canPublish(final ReviewDb db) throws OrmException {
     return (isOwner() || getRefControl().canPublishDrafts())
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 5f18045..6e081ea 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -43,6 +43,9 @@
         flex: 1;
         font: inherit;
       }
+      gr-account-chip {
+        margin-top: .3em;
+      }
       .dropdown {
         background-color: #fff;
         box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
@@ -71,6 +74,11 @@
         padding: 0 .15em;
         text-decoration: none;
       }
+      @media screen and (max-width: 50em), screen and (min-width: 75em) {
+        gr-account-chip:first-of-type {
+          margin-top: 0;
+        }
+      }
     </style>
     <gr-ajax id="autocompleteXHR"
         url="[[_computeAutocompleteURL(change)]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index f9ba529..360c281 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -17,30 +17,50 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-account-link/gr-account-link.html">
 <link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-account-chip">
   <template>
     <style>
       :host {
-        display: inline-block;
-        background: #ccc;
-        border-radius: .75em;
+        display: block;
+        overflow: hidden;
       }
-      span.innerspan {
-        margin: .5em;
+      .container {
+        align-items: center;
+        background: #eee;
+        border-radius: .75em;
+        display: inline-flex;
+        padding: 0 .5em;
+      }
+      :host([show-avatar]) .container {
+        padding-left: 0;
+      }
+      gr-button.remove,
+      gr-button.remove:hover,
+      gr-button.remove:focus {
+        border-color: transparent;
+        color: #333;
       }
       gr-button.remove {
+        background: #eee;
+        color: #666;
+        font-size: 1.7em;
+        font-weight: normal;
+        height: .6em;
+        line-height: .6em;
+        margin-left: .15em;
         padding: 0;
         text-decoration: none;
-        background: #ccc;
       }
     </style>
-    <span class="innerspan">
+    <div class="container">
       <gr-account-link account="[[account]]"></gr-account-link>
       <gr-button
-         hidden$="[[!removable]]" hidden
-         class="remove" on-tap="_handleRemoveTap">×</gr-button>
-    </span>
+          hidden$="[[!removable]]" hidden
+          class="remove" on-tap="_handleRemoveTap">×</gr-button>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-account-chip.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 4e32bc6..f5d7ee7 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -24,11 +24,27 @@
         type: Boolean,
         value: false,
       },
+      showAvatar: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
+    },
+
+    ready: function() {
+      this._getHasAvatars().then(function(hasAvatars) {
+        this.showAvatar = hasAvatars;
+      }.bind(this));
     },
 
     _handleRemoveTap: function(e) {
       e.preventDefault();
       this.fire('remove', {account: this.account}, {bubbles: false});
     },
+
+    _getHasAvatars: function() {
+      return this.$.restAPI.getConfig().then(function(cfg) {
+        return Promise.resolve(!!(cfg && cfg.plugin && cfg.plugin.has_avatars));
+      });
+    },
   });
 })();