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:

committed by
Edwin Kempin

parent
6f7ccf5409
commit
beb0b84af8
@@ -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\}
|
||||
~~~~~~~~~~~~
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
}
|
@@ -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; }-*/;
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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}
|
||||
|
@@ -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());
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user