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));
+ });
+ },
});
})();