Allow to remove specific scores while leaving the reviewer listed

Currently only removal of reviewers is supported. This change implements
removal of specific scores while leaving the reviewer still listed on
the change.

Reviewer API is added for listing and deleting votes.

Bug: Issue 3035
Change-Id: Ie4f05fafb73a32835708a76eb86cfacf8ff5c670
This commit is contained in:
David Ostrovsky
2014-12-13 00:24:29 +01:00
committed by Edwin Kempin
parent 6f7ccf5409
commit beb0b84af8
17 changed files with 669 additions and 25 deletions

View File

@@ -2264,6 +2264,55 @@ Deletes a reviewer from a change.
HTTP/1.1 204 No Content
----
[[list-votes]]
=== List Votes
--
'GET /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/'
--
Lists the votes for a specific reviewer of the change.
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0
----
As result a map is returned that maps the label name to the label value.
The entries in the map are sorted by label name.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"Code-Review": -1,
"Verified": 1
"Work-In-Progress": 1,
}
----
[[delete-vote]]
=== Delete Vote
--
'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]'
--
Deletes a single vote from a change. Note, that even when the last vote of
a reviewer is removed the reviewer itself is still listed on the change.
.Request
----
DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
----
.Response
----
HTTP/1.1 204 No Content
----
[[revision-endpoints]]
== Revision Endpoints
@@ -3747,6 +3796,10 @@ UUID of a published comment.
=== \{draft-id\}
UUID of a draft comment.
[[label-id]]
=== \{label-id\}
The name of the label.
[[file-id]]
\{file-id\}
~~~~~~~~~~~~

View File

@@ -26,6 +26,8 @@ import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -46,7 +48,9 @@ import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -63,6 +67,7 @@ import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@NoHttpd
public class ChangeIT extends AbstractDaemonTest {
@@ -378,6 +383,115 @@ public class ChangeIT extends AbstractDaemonTest {
}
}
@Test
public void listVotes() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.approve());
Map<String, Short> m = gApi.changes()
.id(r.getChangeId())
.reviewer(admin.getId().toString())
.votes();
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", new Short((short)2));
setApiUser(user);
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.dislike());
m = gApi.changes()
.id(r.getChangeId())
.reviewer(user.getId().toString())
.votes();
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", new Short((short)-1));
}
@Test
public void deleteVote() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.approve());
setApiUser(user);
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.recommend());
setApiUser(admin);
gApi.changes()
.id(r.getChangeId())
.reviewer(admin.getId().toString())
.deleteVote("Code-Review");
Map<String, Short> m = gApi.changes()
.id(r.getChangeId())
.reviewer(admin.getId().toString())
.votes();
if (isNoteDbTestEnabled()) {
// When notedb is enabled each reviewer is explicitly recorded in the
// notedb and this record stays even when all votes of that user have been
// deleted, hence there is no dummy 0 approval left when a vote is
// deleted.
assertThat(m).isEmpty();
} else {
// When notedb is disabled there is a dummy 0 approval on the change so
// that the user is still returned as CC when all votes of that user have
// been deleted.
assertThat(m).containsEntry("Code-Review", new Short((short)0));
}
ChangeInfo c = gApi.changes()
.id(r.getChangeId())
.get();
assertThat(Iterables.getLast(c.messages).message).isEqualTo(
"Removed Code-Review+2 by Administrator <admin@example.com>\n");
if (isNoteDbTestEnabled()) {
// When notedb is enabled each reviewer is explicitly recorded in the
// notedb and this record stays even when all votes of that user have been
// deleted.
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(
ImmutableSet.of(admin.getId(), user.getId()));
} else {
// When notedb is disabled users that have only dummy 0 approvals on the
// change are returned as CC and not as REVIEWER.
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(user.getId()));
assertThat(getReviewers(c.reviewers.get(CC)))
.containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
}
}
@Test
public void deleteVoteNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.approve());
setApiUser(user);
exception.expect(AuthException.class);
exception.expectMessage("delete not permitted");
gApi.changes()
.id(r.getChangeId())
.reviewer(admin.getId().toString())
.deleteVote("Code-Review");
}
@Test
public void createEmptyChange() throws Exception {
ChangeInfo in = new ChangeInfo();
@@ -677,4 +791,15 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(rev2.pushCertificate.certificate).isNull();
assertThat(rev2.pushCertificate.key).isNull();
}
private static Iterable<Account.Id> getReviewers(
Collection<AccountInfo> r) {
return Iterables.transform(r, new Function<AccountInfo, Account.Id>() {
@Override
public Account.Id apply(AccountInfo account) {
return new Account.Id(account._accountId);
}
});
}
}

View File

@@ -58,6 +58,19 @@ public interface ChangeApi {
*/
RevisionApi revision(String id) throws RestApiException;
/**
* Look up the reviewer of the change.
* <p>
* @param id ID of the account, can be a string of the format
* "Full Name <mail@example.com>", just the email address, a full name
* if it is unique, an account ID, a user name or 'self' for the
* calling user.
* @return API for accessing the reviewer.
* @throws RestApiException if id is not account ID or is a user that isn't
* known to be a reviewer for this change.
*/
ReviewerApi reviewer(String id) throws RestApiException;
void abandon() throws RestApiException;
void abandon(AbandonInput in) throws RestApiException;
@@ -176,6 +189,11 @@ public interface ChangeApi {
throw new NotImplementedException();
}
@Override
public ReviewerApi reviewer(String id) throws RestApiException {
throw new NotImplementedException();
}
@Override
public RevisionApi revision(String id) throws RestApiException {
throw new NotImplementedException();

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2014 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;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Map;
public interface ReviewerApi {
Map<String, Short> votes() throws RestApiException;
void deleteVote(String label) throws RestApiException;
}

View File

@@ -31,6 +31,7 @@ import java.sql.Timestamp;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -84,6 +85,16 @@ public class ChangeInfo extends JavaScriptObject {
return allLabels().keySet();
}
public final Set<Integer> removableReviewerIds() {
Set<Integer> removable = new HashSet<>();
if (removableReviewers() != null) {
for (AccountInfo a : Natives.asList(removableReviewers())) {
removable.add(a._accountId());
}
}
return removable;
}
public final native String id() /*-{ return this.id; }-*/;
public final native String project() /*-{ return this.project; }-*/;
public final native String branch() /*-{ return this.branch; }-*/;

View File

@@ -48,20 +48,26 @@ import java.util.Set;
/** Displays a table of label and reviewer scores. */
class Labels extends Grid {
private static final String DATA_ID = "data-id";
private static final String REMOVE;
private static final String DATA_VOTE = "data-vote";
private static final String REMOVE_REVIEWER;
private static final String REMOVE_VOTE;
static {
REMOVE = DOM.createUniqueId().replace('-', '_');
init(REMOVE);
REMOVE_REVIEWER = DOM.createUniqueId().replace('-', '_');
REMOVE_VOTE = DOM.createUniqueId().replace('-', '_');
init(REMOVE_REVIEWER, REMOVE_VOTE);
}
private static final native void init(String r) /*-{
private static final native void init(String r, String v) /*-{
$wnd[r] = $entry(function(e) {
@com.google.gerrit.client.change.Labels::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
@com.google.gerrit.client.change.Labels::onRemoveReviewer(Lcom/google/gwt/dom/client/NativeEvent;)(e)
});
$wnd[v] = $entry(function(e) {
@com.google.gerrit.client.change.Labels::onRemoveVote(Lcom/google/gwt/dom/client/NativeEvent;)(e)
});
}-*/;
private static void onRemove(NativeEvent event) {
private static void onRemoveReviewer(NativeEvent event) {
Integer user = getDataId(event);
if (user != null) {
final ChangeScreen screen = ChangeScreen.get(event);
@@ -77,6 +83,23 @@ class Labels extends Grid {
}
}
private static void onRemoveVote(NativeEvent event) {
Integer user = getDataId(event);
String vote = getVoteId(event);
if (user != null && vote != null) {
final ChangeScreen screen = ChangeScreen.get(event);
ChangeApi.vote(screen.getChangeId().get(), user, vote).delete(
new GerritCallback<JavaScriptObject>() {
@Override
public void onSuccess(JavaScriptObject result) {
if (screen.isCurrentView()) {
Gerrit.display(PageLinks.toChange(screen.getChangeId()));
}
}
});
}
}
private static Integer getDataId(NativeEvent event) {
Element e = event.getEventTarget().cast();
while (e != null) {
@@ -89,6 +112,18 @@ class Labels extends Grid {
return null;
}
private static String getVoteId(NativeEvent event) {
Element e = event.getEventTarget().cast();
while (e != null) {
String v = e.getAttribute(DATA_VOTE);
if (!v.isEmpty()) {
return v;
}
e = e.getParentElement();
}
return null;
}
private ChangeScreen.Style style;
void init(ChangeScreen.Style style) {
@@ -97,6 +132,7 @@ class Labels extends Grid {
void set(ChangeInfo info) {
List<String> names = new ArrayList<>(info.labels());
Set<Integer> removable = info.removableReviewerIds();
Collections.sort(names);
resize(names.size(), 2);
@@ -106,14 +142,14 @@ class Labels extends Grid {
LabelInfo label = info.label(name);
setText(row, 0, name);
if (label.all() != null) {
setWidget(row, 1, renderUsers(label));
setWidget(row, 1, renderUsers(label, removable));
}
getCellFormatter().setStyleName(row, 0, style.labelName());
getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
}
}
private Widget renderUsers(LabelInfo label) {
private Widget renderUsers(LabelInfo label, Set<Integer> removable) {
Map<Integer, List<ApprovalInfo>> m = new HashMap<>(4);
int approved = 0;
int rejected = 0;
@@ -150,8 +186,8 @@ class Labels extends Grid {
html.setStyleName(style.label_reject());
}
html.append(val).append(" ");
html.append(formatUserList(style, m.get(v),
Collections.<Integer> emptySet(), null));
html.append(formatUserList(style, m.get(v), removable,
label.name(), null));
html.closeSpan();
}
return html.toBlockWidget();
@@ -198,6 +234,7 @@ class Labels extends Grid {
static SafeHtml formatUserList(ChangeScreen.Style style,
Collection<? extends AccountInfo> in,
Set<Integer> removable,
String label,
Map<Integer, VotableInfo> votable) {
List<AccountInfo> users = new ArrayList<>(in);
Collections.sort(users, new Comparator<AccountInfo>() {
@@ -257,6 +294,9 @@ class Labels extends Grid {
.setAttribute(DATA_ID, ai._accountId())
.setAttribute("title", getTitle(ai, votableCategories))
.setStyleName(style.label_user());
if (label != null) {
html.setAttribute(DATA_VOTE, label);
}
if (img != null) {
html.openElement("img")
.setStyleName(style.avatar())
@@ -271,10 +311,15 @@ class Labels extends Grid {
}
html.append(name);
if (removable.contains(ai._accountId())) {
html.openElement("button")
.setAttribute("title", Util.M.removeReviewer(name))
.setAttribute("onclick", REMOVE + "(event)")
.append("×")
html.openElement("button");
if (label != null) {
html.setAttribute("title", Util.M.removeVote(label))
.setAttribute("onclick", REMOVE_VOTE + "(event)");
} else {
html.setAttribute("title", Util.M.removeReviewer(name))
.setAttribute("onclick", REMOVE_REVIEWER + "(event)");
}
html.append("×")
.closeElement("button");
}
html.closeSpan();

View File

@@ -51,7 +51,6 @@ import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -214,20 +213,13 @@ public class Reviewers extends Composite {
cc.remove(i);
}
cc.remove(info.owner()._accountId());
Set<Integer> removable = new HashSet<>();
if (info.removableReviewers() != null) {
for (AccountInfo a : Natives.asList(info.removableReviewers())) {
removable.add(a._accountId());
}
}
Set<Integer> removable = info.removableReviewerIds();
Map<Integer, VotableInfo> votable = votable(info);
SafeHtml rHtml = Labels.formatUserList(style,
r.values(), removable, votable);
r.values(), removable, null, votable);
SafeHtml ccHtml = Labels.formatUserList(style,
cc.values(), removable, votable);
cc.values(), removable, null, votable);
reviewersText.setInnerSafeHtml(rHtml);
ccText.setInnerSafeHtml(ccHtml);

View File

@@ -155,6 +155,10 @@ public class ChangeApi {
.addParameter("n", n);
}
public static RestApi vote(int id, int reviewer, String vote) {
return reviewer(id, reviewer).view("votes").id(vote);
}
public static RestApi reviewer(int id, int reviewer) {
return change(id).view("reviewers").id(reviewer);
}

View File

@@ -43,6 +43,7 @@ public interface ChangeMessages extends Messages {
String removeHashtag(String name);
String removeReviewer(String fullName);
String removeVote(String label);
String messageWrittenOn(String date);
String renamedFrom(String sourcePath);

View File

@@ -24,6 +24,7 @@ patchTableSize_Lines = {0} lines
removeHashtag = Remove hashtag {0}
removeReviewer = Remove reviewer {0}
removeVote = Remove vote {0}
messageWrittenOn = on {0}
renamedFrom = renamed from {0}

View File

@@ -22,6 +22,7 @@ import com.google.gerrit.extensions.api.changes.FixInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.RestoreInput;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.changes.ReviewerApi;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -46,6 +47,7 @@ import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.PutTopic;
import com.google.gerrit.server.change.Restore;
import com.google.gerrit.server.change.Revert;
import com.google.gerrit.server.change.Reviewers;
import com.google.gerrit.server.change.Revisions;
import com.google.gerrit.server.change.SubmittedTogether;
import com.google.gerrit.server.change.SuggestReviewers;
@@ -68,7 +70,9 @@ class ChangeApiImpl implements ChangeApi {
private final Provider<CurrentUser> user;
private final Changes changeApi;
private final Reviewers reviewers;
private final Revisions revisions;
private final ReviewerApiImpl.Factory reviewerApi;
private final RevisionApiImpl.Factory revisionApi;
private final Provider<SuggestReviewers> suggestReviewers;
private final ChangeResource change;
@@ -90,7 +94,9 @@ class ChangeApiImpl implements ChangeApi {
@Inject
ChangeApiImpl(Provider<CurrentUser> user,
Changes changeApi,
Reviewers reviewers,
Revisions revisions,
ReviewerApiImpl.Factory reviewerApi,
RevisionApiImpl.Factory revisionApi,
Provider<SuggestReviewers> suggestReviewers,
Abandon abandon,
@@ -111,7 +117,9 @@ class ChangeApiImpl implements ChangeApi {
this.user = user;
this.changeApi = changeApi;
this.revert = revert;
this.reviewers = reviewers;
this.revisions = revisions;
this.reviewerApi = reviewerApi;
this.revisionApi = revisionApi;
this.suggestReviewers = suggestReviewers;
this.abandon = abandon;
@@ -155,6 +163,16 @@ class ChangeApiImpl implements ChangeApi {
}
}
@Override
public ReviewerApi reviewer(String id) throws RestApiException {
try {
return reviewerApi.create(
reviewers.parse(change, IdString.fromDecoded(id)));
} catch (OrmException e) {
throw new RestApiException("Cannot parse reviewer", e);
}
}
@Override
public void abandon() throws RestApiException {
abandon(new AbandonInput());

View File

@@ -27,5 +27,6 @@ public class Module extends FactoryModule {
factory(DraftApiImpl.Factory.class);
factory(RevisionApiImpl.Factory.class);
factory(FileApiImpl.Factory.class);
factory(ReviewerApiImpl.Factory.class);
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (C) 2014 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.api.changes;
import com.google.gerrit.extensions.api.changes.ReviewerApi;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.change.DeleteVote;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.VoteResource;
import com.google.gerrit.server.change.Votes;
import com.google.gerrit.server.git.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.util.Map;
public class ReviewerApiImpl implements ReviewerApi {
interface Factory {
ReviewerApiImpl create(ReviewerResource r);
}
private final ReviewerResource reviewer;
private final Votes.List listVotes;
private final DeleteVote deleteVote;
@Inject
ReviewerApiImpl(Votes.List listVotes,
DeleteVote deleteVote,
@Assisted ReviewerResource reviewer) {
this.listVotes = listVotes;
this.deleteVote = deleteVote;
this.reviewer = reviewer;
}
@Override
public Map<String, Short> votes() throws RestApiException {
try {
return listVotes.apply(reviewer);
} catch (OrmException e) {
throw new RestApiException("Cannot list votes", e);
}
}
@Override
public void deleteVote(String label) throws RestApiException {
try {
deleteVote.apply(new VoteResource(reviewer, label), null);
} catch (UpdateException e) {
throw new RestApiException("Cannot delete vote", e);
}
}
}

View File

@@ -0,0 +1,151 @@
// Copyright (C) 2014 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.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
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.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.DeleteVote.Input;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collections;
@Singleton
public class DeleteVote implements RestModifyView<VoteResource, Input> {
public static class Input {
}
private final Provider<ReviewDb> db;
private final BatchUpdate.Factory batchUpdateFactory;
private final ApprovalsUtil approvalsUtil;
private final ChangeMessagesUtil cmUtil;
private final IdentifiedUser.GenericFactory userFactory;
@Inject
DeleteVote(Provider<ReviewDb> db,
BatchUpdate.Factory batchUpdateFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
IdentifiedUser.GenericFactory userFactory) {
this.db = db;
this.batchUpdateFactory = batchUpdateFactory;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
this.userFactory = userFactory;
}
@Override
public Response<?> apply(VoteResource rsrc, Input input)
throws RestApiException, UpdateException {
ReviewerResource r = rsrc.getReviewer();
ChangeControl ctl = r.getControl();
Change change = r.getChange();
try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
change.getProject(), ctl.getUser().asIdentifiedUser(),
TimeUtil.nowTs())) {
bu.addOp(change.getId(),
new Op(r.getUser().getAccountId(), rsrc.getLabel()));
bu.execute();
}
return Response.none();
}
private class Op extends BatchUpdate.Op {
private final Account.Id accountId;
private final String label;
private Op(Account.Id accountId, String label) {
this.accountId = accountId;
this.label = label;
}
@Override
public void updateChange(ChangeContext ctx)
throws OrmException, AuthException, ResourceNotFoundException {
IdentifiedUser user = ctx.getUser().asIdentifiedUser();
Change change = ctx.getChange();
ChangeControl ctl = ctx.getChangeControl();
PatchSet.Id psId = change.currentPatchSetId();
PatchSetApproval psa = null;
StringBuilder msg = new StringBuilder();
for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
ctx.getDb(), ctl, psId, accountId)) {
if (ctl.canRemoveReviewer(a)) {
if (a.getLabel().equals(label)) {
msg.append("Removed ")
.append(a.getLabel()).append(formatLabelValue(a.getValue()))
.append(" by ").append(userFactory.create(user.getAccountId())
.getNameEmail())
.append("\n");
psa = a;
a.setValue((short)0);
ctx.getChangeUpdate().setPatchSetId(psId);
ctx.getChangeUpdate().removeApproval(label);
break;
}
} else {
throw new AuthException("delete not permitted");
}
}
if (psa == null) {
throw new ResourceNotFoundException();
}
ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), ctx.getDb());
ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
if (msg.length() > 0) {
ChangeMessage changeMessage =
new ChangeMessage(new ChangeMessage.Key(change.getId(),
ChangeUtil.messageUUID(ctx.getDb())),
user.getAccountId(),
ctx.getWhen(),
change.currentPatchSetId());
changeMessage.setMessage(msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(),
changeMessage);
}
}
}
private static String formatLabelValue(short value) {
if (value > 0) {
return "+" + value;
} else {
return Short.toString(value);
}
}
}

View File

@@ -21,6 +21,7 @@ import static com.google.gerrit.server.change.DraftCommentResource.DRAFT_COMMENT
import static com.google.gerrit.server.change.FileResource.FILE_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
import static com.google.gerrit.server.change.VoteResource.VOTE_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -37,6 +38,7 @@ public class Module extends RestApiModule {
bind(DraftComments.class);
bind(Comments.class);
bind(Files.class);
bind(Votes.class);
DynamicMap.mapOf(binder(), CHANGE_KIND);
DynamicMap.mapOf(binder(), COMMENT_KIND);
@@ -45,6 +47,7 @@ public class Module extends RestApiModule {
DynamicMap.mapOf(binder(), REVIEWER_KIND);
DynamicMap.mapOf(binder(), REVISION_KIND);
DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
DynamicMap.mapOf(binder(), VOTE_KIND);
get(CHANGE_KIND).to(GetChange.class);
get(CHANGE_KIND, "detail").to(GetDetail.class);
@@ -73,6 +76,8 @@ public class Module extends RestApiModule {
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
get(REVIEWER_KIND).to(GetReviewer.class);
delete(REVIEWER_KIND).to(DeleteReviewer.class);
child(REVIEWER_KIND, "votes").to(Votes.class);
delete(VOTE_KIND).to(DeleteVote.class);
child(CHANGE_KIND, "revisions").to(Revisions.class);
get(REVISION_KIND, "actions").to(GetRevisionActions.class);

View File

@@ -0,0 +1,40 @@
// Copyright (C) 2014 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.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.inject.TypeLiteral;
public class VoteResource implements RestResource {
public static final TypeLiteral<RestView<VoteResource>> VOTE_KIND =
new TypeLiteral<RestView<VoteResource>>() {};
private final ReviewerResource reviewer;
private final String label;
public VoteResource(ReviewerResource reviewer, String label) {
this.reviewer = reviewer;
this.label = label;
}
public ReviewerResource getReviewer() {
return reviewer;
}
public String getLabel() {
return label;
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2014 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.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Map;
import java.util.TreeMap;
@Singleton
public class Votes implements ChildCollection<ReviewerResource, VoteResource> {
private final DynamicMap<RestView<VoteResource>> views;
private final List list;
@Inject
Votes(DynamicMap<RestView<VoteResource>> views,
List list) {
this.views = views;
this.list = list;
}
@Override
public DynamicMap<RestView<VoteResource>> views() {
return views;
}
@Override
public RestView<ReviewerResource> list() throws AuthException {
return list;
}
@Override
public VoteResource parse(ReviewerResource reviewer, IdString id)
throws ResourceNotFoundException, OrmException, AuthException {
return new VoteResource(reviewer, id.get());
}
@Singleton
public static class List implements RestReadView<ReviewerResource> {
private final Provider<ReviewDb> db;
private final ApprovalsUtil approvalsUtil;
@Inject
List(Provider<ReviewDb> db,
ApprovalsUtil approvalsUtil) {
this.db = db;
this.approvalsUtil = approvalsUtil;
}
@Override
public Map<String, Short> apply(ReviewerResource rsrc) throws OrmException {
Map<String, Short> votes = new TreeMap<>();
Iterable<PatchSetApproval> byPatchSetUser = approvalsUtil.byPatchSetUser(
db.get(),
rsrc.getControl(),
rsrc.getChange().currentPatchSetId(),
rsrc.getUser().getAccountId());
for (PatchSetApproval psa : byPatchSetUser) {
votes.put(psa.getLabel(), psa.getValue());
}
return votes;
}
}
}