Append approval info to every comment-added stream event and hook

This change is driven by the RFC proposal[1] to show label
transitions more explicitly in Gerrit. Stream events only
provides label information on review events that contain
a vote transition.  When a user submits a review with only a
reply message the approvals information is not added to the
stream event and hooks. This change will make Gerrit append
approvals info to every comment-added event including the
ones that do not contain vote changes.

Since labels are shown for every event we will add an 'oldValue'
property to the approvalAttribute to distinguish when a vote
transition has occurred.  The 'oldValue' attribute contains
the previous vote score.  This attribute will only appear when
there is a vote transition.

The comment-added stream event will look like this:

  "approvals":[{"type":"Code-Review","description":"Code-Review",
  "value":"-1", "oldValue":"0"}, {"type":"Verified",
  "description":"Verified","value":"0"}],
  "comment":"Patch Set 1: Code-Review+1\n\nMy Message" ...

This change will also a '--$LabelName-oldValue' parameter to
corresponding comment-added change hook:

  hook[comment-added] output:
  --change I2cd8327360ff89338a4e7f0591bf0c037e9aa5db
  --is-draft false --change-url http://localhost:8080/201
  --change-owner John Smith (jsmith@gmail.com) --project okc
  --branch master --author John Smith (jsmith@gmail.com)
  --commit 55079187f7ce1c461de804c13f2dd71d64a85280
  --comment Patch Set 1: Code-Review-1 --Code-Review -1
  --Code-Review-oldValue 0 --Verified 0

[1] https://groups.google.com/d/msg/repo-discuss/soqmGjRpl-4/IIfAF4jbCQAJ

Bug: Issue 3220
Change-Id: Ibc1c64d70e790c7b507db79931f31fd77f25e276
This commit is contained in:
Khai Do
2015-06-04 14:04:27 -07:00
parent 5992220391
commit 71b58990e9
12 changed files with 452 additions and 29 deletions

View File

@@ -67,7 +67,7 @@ This is called whenever a draft change is published.
This is called whenever a comment is added to a change. This is called whenever a comment is added to a change.
==== ====
comment-added --change <change id> --is-draft <boolean> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...] comment-added --change <change id> --is-draft <boolean> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> --<approval category id>-oldValue <score> ...]
==== ====
=== change-merged === change-merged

View File

@@ -140,6 +140,8 @@ description:: Human readable category of the approval.
value:: Value assigned by the approval, usually a numerical score. value:: Value assigned by the approval, usually a numerical score.
oldValue:: The previous approval score, only present if the value changed as a result of this event.
grantedOn:: Time in seconds since the UNIX epoch when this approval grantedOn:: Time in seconds since the UNIX epoch when this approval
was added or last updated. was added or last updated.

View File

@@ -0,0 +1,253 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance.server.event;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
import static org.junit.Assert.fail;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.UserScopedEventListener;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.data.ApprovalAttribute;
import com.google.gerrit.server.events.CommentAddedEvent;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.Util;
import com.google.inject.Inject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@NoHttpd
public class CommentAddedEventIT extends AbstractDaemonTest {
@Inject
private IdentifiedUser.GenericFactory factory;
@Inject
private DynamicSet<UserScopedEventListener> source;
private final LabelType label = category("CustomLabel",
value(1, "Positive"),
value(0, "No score"),
value(-1, "Negative"));
private final LabelType pLabel = category("CustomLabel2",
value(1, "Positive"),
value(0, "No score"));
private RegistrationHandle eventListenerRegistration;
private CommentAddedEvent lastCommentAddedEvent;
@Before
public void setUp() throws Exception {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
AccountGroup.UUID anonymousUsers =
SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers,
"refs/heads/*");
Util.allow(cfg, Permission.forLabel(pLabel.getName()), 0, 1, anonymousUsers,
"refs/heads/*");
saveProjectConfig(project, cfg);
eventListenerRegistration = source.add(new UserScopedEventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof CommentAddedEvent) {
lastCommentAddedEvent = (CommentAddedEvent) event;
}
}
@Override
public CurrentUser getUser() {
return factory.create(user.id);
}
});
}
@After
public void cleanup() {
eventListenerRegistration.remove();
}
private void saveLabelConfig() throws Exception {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
cfg.getLabelSections().put(label.getName(), label);
cfg.getLabelSections().put(pLabel.getName(), pLabel);
saveProjectConfig(project, cfg);
}
@Test
public void newChangeWithVote() throws Exception {
saveLabelConfig();
// push a new change with -1 vote
PushOneCommit.Result r = createChange();
ReviewInput reviewInput = new ReviewInput().label(
label.getName(), (short)-1);
revision(r).review(reviewInput);
String newVote = lastCommentAddedEvent.approvals.get()[0].value;
String oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo("0");
assertThat(newVote).isEqualTo("-1");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: %s-1", label.getName()));
}
@Test
public void newPatchSetWithVote() throws Exception {
saveLabelConfig();
// push a new change
PushOneCommit.Result r = createChange();
ReviewInput reviewInput = new ReviewInput().message(label.getName());
revision(r).review(reviewInput);
// push a new revision with +1 vote
ChangeInfo c = get(r.getChangeId());
r = amendChange(c.changeId);
reviewInput = new ReviewInput().label(
label.getName(), (short)1);
revision(r).review(reviewInput);
String newVote = lastCommentAddedEvent.approvals.get()[0].value;
String oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo("0");
assertThat(newVote).isEqualTo("1");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 2: %s+1", label.getName()));
}
@Test
public void reviewChange() throws Exception {
saveLabelConfig();
// push a change
PushOneCommit.Result r = createChange();
// review with message only, do not apply votes
ReviewInput reviewInput = new ReviewInput().message(label.getName());
revision(r).review(reviewInput);
// reply message only so votes are excluded from comment
assertThat(lastCommentAddedEvent.approvals.get()).isNull();
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1:\n\n%s", label.getName()));
// transition from un-voted to -1 vote
reviewInput = new ReviewInput().label(label.getName(), -1);
revision(r).review(reviewInput);
String newVote = lastCommentAddedEvent.approvals.get()[0].value;
String oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo("0");
assertThat(newVote).isEqualTo("-1");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: %s-1", label.getName()));
// transition vote from -1 to 0
reviewInput = new ReviewInput().label(label.getName(), 0);
revision(r).review(reviewInput);
newVote = lastCommentAddedEvent.approvals.get()[0].value;
oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo("-1");
assertThat(newVote).isEqualTo("0");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: -%s", label.getName()));
// transition vote from 0 to 1
reviewInput = new ReviewInput().label(label.getName(), 1);
revision(r).review(reviewInput);
newVote = lastCommentAddedEvent.approvals.get()[0].value;
oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo("0");
assertThat(newVote).isEqualTo("1");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: %s+1", label.getName()));
// transition vote from 1 to -1
reviewInput = new ReviewInput().label(label.getName(), -1);
revision(r).review(reviewInput);
newVote = lastCommentAddedEvent.approvals.get()[0].value;
oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo("1");
assertThat(newVote).isEqualTo("-1");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: %s-1", label.getName()));
// review with message only, do not apply votes
reviewInput = new ReviewInput().message(label.getName());
revision(r).review(reviewInput);
newVote = lastCommentAddedEvent.approvals.get()[0].value;
oldVote = lastCommentAddedEvent.approvals.get()[0].oldValue;
assertThat(oldVote).isEqualTo(null); // no vote change so not included
assertThat(newVote).isEqualTo("-1");
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1:\n\n%s", label.getName()));
}
@Test
public void reviewChange_MultipleVotes() throws Exception {
saveLabelConfig();
PushOneCommit.Result r = createChange();
ReviewInput reviewInput = new ReviewInput().label(label.getName(), -1);
reviewInput.message = label.getName();
revision(r).review(reviewInput);
ChangeInfo c = get(r.getChangeId());
LabelInfo q = c.labels.get(label.getName());
assertThat(q.all).hasSize(1);
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: %s-1\n\n%s",
label.getName(), label.getName()));
reviewInput = new ReviewInput().label(pLabel.getName(), 1);
reviewInput.message = pLabel.getName();
revision(r).review(reviewInput);
c = get(r.getChangeId());
q = c.labels.get(label.getName());
assertThat(q.all).hasSize(1);
assertThat(lastCommentAddedEvent.comment).isEqualTo(
String.format("Patch Set 1: %s+1\n\n%s",
pLabel.getName(), pLabel.getName()));
assertThat(lastCommentAddedEvent.approvals.get()).hasLength(2);
for (ApprovalAttribute approval : lastCommentAddedEvent.approvals.get()) {
if (approval.type.equals(label.getName())) {
assertThat(approval.value).isEqualTo("-1");
assertThat(approval.oldValue).isNull();
} else if (approval.type.equals(pLabel.getName())) {
assertThat(approval.value).isEqualTo("1");
assertThat(approval.oldValue).isEqualTo("0");
} else {
fail("Unexpected label: " + approval.type);
}
}
}
}

View File

@@ -22,23 +22,38 @@ import static com.google.gerrit.server.project.Util.value;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.UserScopedEventListener;
import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.events.CommentAddedEvent;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.Util; import com.google.gerrit.server.project.Util;
import com.google.inject.Inject;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@NoHttpd @NoHttpd
public class CustomLabelIT extends AbstractDaemonTest { public class CustomLabelIT extends AbstractDaemonTest {
@Inject
private IdentifiedUser.GenericFactory factory;
@Inject
private DynamicSet<UserScopedEventListener> source;
private final LabelType label = category("CustomLabel", private final LabelType label = category("CustomLabel",
value(1, "Positive"), value(1, "Positive"),
value(0, "No score"), value(0, "No score"),
@@ -48,6 +63,9 @@ public class CustomLabelIT extends AbstractDaemonTest {
value(1, "Positive"), value(1, "Positive"),
value(0, "No score")); value(0, "No score"));
private CommentAddedEvent lastCommentAddedEvent;
private RegistrationHandle eventListenerRegistration;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
@@ -58,6 +76,26 @@ public class CustomLabelIT extends AbstractDaemonTest {
Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers, Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers,
"refs/heads/*"); "refs/heads/*");
saveProjectConfig(project, cfg); saveProjectConfig(project, cfg);
eventListenerRegistration = source.add(new UserScopedEventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof CommentAddedEvent) {
lastCommentAddedEvent = (CommentAddedEvent) event;
}
}
@Override
public CurrentUser getUser() {
return factory.create(user.id);
}
});
}
@After
public void cleanup() {
eventListenerRegistration.remove();
db.close();
} }
@Test @Test
@@ -124,13 +162,18 @@ public class CustomLabelIT extends AbstractDaemonTest {
.id(r.getChangeId()) .id(r.getChangeId())
.addReviewer(in); .addReviewer(in);
revision(r).review(new ReviewInput().label(P.getName(), 0)); ReviewInput input = new ReviewInput().label(P.getName(), 0);
input.message = "foo";
revision(r).review(input);
ChangeInfo c = get(r.getChangeId()); ChangeInfo c = get(r.getChangeId());
LabelInfo q = c.labels.get(P.getName()); LabelInfo q = c.labels.get(P.getName());
assertThat(q.all).hasSize(2); assertThat(q.all).hasSize(2);
assertThat(q.disliked).isNull(); assertThat(q.disliked).isNull();
assertThat(q.rejected).isNull(); assertThat(q.rejected).isNull();
assertThat(q.blocking).isNull(); assertThat(q.blocking).isNull();
assertThat(lastCommentAddedEvent.comment).isEqualTo(
"Patch Set 1:\n\n" + input.message);
} }
@Test @Test

View File

@@ -423,7 +423,8 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener,
@Override @Override
public void doCommentAddedHook(final Change change, Account account, public void doCommentAddedHook(final Change change, Account account,
PatchSet patchSet, String comment, final Map<String, Short> approvals, PatchSet patchSet, String comment, final Map<String, Short> approvals,
ReviewDb db) throws OrmException { final Map<String, Short> oldApprovals, ReviewDb db)
throws OrmException {
CommentAddedEvent event = new CommentAddedEvent(change); CommentAddedEvent event = new CommentAddedEvent(change);
Supplier<AccountState> owner = getAccountSupplier(change.getOwner()); Supplier<AccountState> owner = getAccountSupplier(change.getOwner());
@@ -441,7 +442,8 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener,
ApprovalAttribute[] r = new ApprovalAttribute[approvals.size()]; ApprovalAttribute[] r = new ApprovalAttribute[approvals.size()];
int i = 0; int i = 0;
for (Map.Entry<String, Short> approval : approvals.entrySet()) { for (Map.Entry<String, Short> approval : approvals.entrySet()) {
r[i++] = getApprovalAttribute(labelTypes, approval); r[i++] = getApprovalAttribute(labelTypes, approval,
oldApprovals);
} }
return r; return r;
} }
@@ -473,11 +475,17 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener,
change.getProject()).getLabelTypes(); change.getProject()).getLabelTypes();
for (Map.Entry<String, Short> approval : approvals.entrySet()) { for (Map.Entry<String, Short> approval : approvals.entrySet()) {
LabelType lt = labelTypes.byLabel(approval.getKey()); LabelType lt = labelTypes.byLabel(approval.getKey());
if (lt != null) { if (lt != null && approval.getValue() != null) {
addArg(args, "--" + lt.getName(), Short.toString(approval.getValue())); addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
if (oldApprovals != null && !oldApprovals.isEmpty()) {
Short oldValue = oldApprovals.get(approval.getKey());
if (oldValue != null) {
addArg(args, "--" + lt.getName() + "-oldValue",
Short.toString(oldValue));
}
}
} }
} }
runHook(change.getProject(), commentAddedHook, args); runHook(change.getProject(), commentAddedHook, args);
} }
@@ -856,18 +864,29 @@ public class ChangeHookRunner implements ChangeHooks, LifecycleListener,
/** /**
* Create an ApprovalAttribute for the given approval suitable for serialization to JSON. * Create an ApprovalAttribute for the given approval suitable for serialization to JSON.
* @param labelTypes
* @param approval * @param approval
* @param oldApprovals
* @return object suitable for serialization to JSON * @return object suitable for serialization to JSON
*/ */
private ApprovalAttribute getApprovalAttribute(LabelTypes labelTypes, private ApprovalAttribute getApprovalAttribute(LabelTypes labelTypes,
Entry<String, Short> approval) { Entry<String, Short> approval,
Map<String, Short> oldApprovals) {
ApprovalAttribute a = new ApprovalAttribute(); ApprovalAttribute a = new ApprovalAttribute();
a.type = approval.getKey(); a.type = approval.getKey();
if (oldApprovals != null && !oldApprovals.isEmpty()) {
if (oldApprovals.get(approval.getKey()) != null) {
a.oldValue = Short.toString(oldApprovals.get(approval.getKey()));
}
}
LabelType lt = labelTypes.byLabel(approval.getKey()); LabelType lt = labelTypes.byLabel(approval.getKey());
if (lt != null) { if (lt != null) {
a.description = lt.getName(); a.description = lt.getName();
} }
if (approval.getValue() != null) {
a.value = Short.toString(approval.getValue()); a.value = Short.toString(approval.getValue());
}
return a; return a;
} }

View File

@@ -60,13 +60,15 @@ public interface ChangeHooks {
* @param account The gerrit user who added the comment. * @param account The gerrit user who added the comment.
* @param patchSet The patchset this comment is related to. * @param patchSet The patchset this comment is related to.
* @param comment The comment given. * @param comment The comment given.
* @param approvals Map of label IDs to scores. * @param approvals Map of label IDs to scores
* @param oldApprovals Map of label IDs to old approval scores
* @param db The review database. * @param db The review database.
* @throws OrmException * @throws OrmException
*/ */
void doCommentAddedHook(Change change, Account account, void doCommentAddedHook(Change change, Account account,
PatchSet patchSet, String comment, PatchSet patchSet, String comment,
Map<String, Short> approvals, ReviewDb db) Map<String, Short> approvals, Map<String, Short> oldApprovals,
ReviewDb db)
throws OrmException; throws OrmException;
/** /**

View File

@@ -61,7 +61,8 @@ public final class DisabledChangeHooks implements ChangeHooks, EventDispatcher {
@Override @Override
public void doCommentAddedHook(Change change, Account account, public void doCommentAddedHook(Change change, Account account,
PatchSet patchSet, String comment, PatchSet patchSet, String comment,
Map<String, Short> approvals, ReviewDb db) { Map<String, Short> approvals, Map<String, Short> oldApprovals,
ReviewDb db) {
} }
@Override @Override

View File

@@ -21,6 +21,7 @@ import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.gerrit.common.ChangeHooks; import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -49,6 +50,7 @@ import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.project.RefControl;
@@ -68,6 +70,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -82,6 +85,7 @@ public class ChangeInserter extends BatchUpdate.InsertChangeOp {
LoggerFactory.getLogger(ChangeInserter.class); LoggerFactory.getLogger(ChangeInserter.class);
private final ProjectControl.GenericFactory projectControlFactory; private final ProjectControl.GenericFactory projectControlFactory;
private final ChangeControl.GenericFactory changeControlFactory;
private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetInfoFactory patchSetInfoFactory;
private final PatchSetUtil psUtil; private final PatchSetUtil psUtil;
private final ChangeHooks hooks; private final ChangeHooks hooks;
@@ -121,6 +125,7 @@ public class ChangeInserter extends BatchUpdate.InsertChangeOp {
@Inject @Inject
ChangeInserter(ProjectControl.GenericFactory projectControlFactory, ChangeInserter(ProjectControl.GenericFactory projectControlFactory,
ChangeControl.GenericFactory changeControlFactory,
PatchSetInfoFactory patchSetInfoFactory, PatchSetInfoFactory patchSetInfoFactory,
PatchSetUtil psUtil, PatchSetUtil psUtil,
ChangeHooks hooks, ChangeHooks hooks,
@@ -133,6 +138,7 @@ public class ChangeInserter extends BatchUpdate.InsertChangeOp {
@Assisted RevCommit commit, @Assisted RevCommit commit,
@Assisted String refName) { @Assisted String refName) {
this.projectControlFactory = projectControlFactory; this.projectControlFactory = projectControlFactory;
this.changeControlFactory = changeControlFactory;
this.patchSetInfoFactory = patchSetInfoFactory; this.patchSetInfoFactory = patchSetInfoFactory;
this.psUtil = psUtil; this.psUtil = psUtil;
this.hooks = hooks; this.hooks = hooks;
@@ -355,7 +361,7 @@ public class ChangeInserter extends BatchUpdate.InsertChangeOp {
} }
@Override @Override
public void postUpdate(Context ctx) throws OrmException { public void postUpdate(Context ctx) throws OrmException, NoSuchChangeException {
if (sendMail) { if (sendMail) {
Runnable sender = new Runnable() { Runnable sender = new Runnable() {
@Override @Override
@@ -386,13 +392,33 @@ public class ChangeInserter extends BatchUpdate.InsertChangeOp {
} }
} }
/** For labels that are not set in this operation, show the "current" value
* of 0, and no oldValue as the value was not modified by this operation.
* For labels that are set in this operation, the value was modified, so
* show a transition from an oldValue of 0 to the new value.
**/
if (runHooks) { if (runHooks) {
ReviewDb db = ctx.getDb(); ReviewDb db = ctx.getDb();
hooks.doPatchsetCreatedHook(change, patchSet, db); hooks.doPatchsetCreatedHook(change, patchSet, db);
if (approvals != null && !approvals.isEmpty()) { if (approvals != null && !approvals.isEmpty()) {
ChangeControl changeControl = changeControlFactory.controlFor(
db, change, ctx.getUser());
List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
Map<String, Short> allApprovals = new HashMap<>();
Map<String, Short> oldApprovals = new HashMap<>();
for (LabelType lt : labels){
allApprovals.put(lt.getName(), (short) 0);
oldApprovals.put(lt.getName(), null);
}
for (Map.Entry<String, Short> entry : approvals.entrySet()) {
if (entry.getValue() != 0) {
allApprovals.put(entry.getKey(), entry.getValue());
oldApprovals.put(entry.getKey(), (short) 0);
}
}
hooks.doCommentAddedHook(change, hooks.doCommentAddedHook(change,
ctx.getUser().asIdentifiedUser().getAccount(), patchSet, null, ctx.getUser().asIdentifiedUser().getAccount(), patchSet, null,
approvals, db); allApprovals, oldApprovals, db);
} }
} }
} }

View File

@@ -21,6 +21,7 @@ import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue; import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@@ -82,6 +83,7 @@ import org.slf4j.LoggerFactory;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -350,7 +352,8 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
private ChangeMessage message; private ChangeMessage message;
private List<PatchLineComment> comments = new ArrayList<>(); private List<PatchLineComment> comments = new ArrayList<>();
private List<String> labelDelta = new ArrayList<>(); private List<String> labelDelta = new ArrayList<>();
private Map<String, Short> categories = new HashMap<>(); private Map<String, Short> approvals = new HashMap<>();
private Map<String, Short> oldApprovals = new HashMap<>();
private Op(PatchSet.Id psId, ReviewInput in) { private Op(PatchSet.Id psId, ReviewInput in) {
this.psId = psId; this.psId = psId;
@@ -393,7 +396,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
} }
try { try {
hooks.doCommentAddedHook(notes.getChange(), user.getAccount(), ps, hooks.doCommentAddedHook(notes.getChange(), user.getAccount(), ps,
message.getMessage(), categories, ctx.getDb()); message.getMessage(), approvals, oldApprovals, ctx.getDb());
} catch (OrmException e) { } catch (OrmException e) {
log.warn("ChangeHook.doCommentAddedHook delivery failed", e); log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
} }
@@ -514,43 +517,77 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
return drafts; return drafts;
} }
private Map<String, Short> approvalsByKey(
Collection<PatchSetApproval> patchsetApprovals) {
Map<String, Short> labels = new HashMap<>();
for (PatchSetApproval psa : patchsetApprovals) {
labels.put(psa.getLabel(), psa.getValue());
}
return labels;
}
private boolean updateLabels(ChangeContext ctx) private boolean updateLabels(ChangeContext ctx)
throws OrmException, ResourceConflictException { throws OrmException, ResourceConflictException {
Map<String, Short> labels = in.labels; Map<String, Short> inLabels = MoreObjects.firstNonNull(in.labels,
if (labels == null) { Collections.<String, Short> emptyMap());
labels = Collections.emptyMap();
}
List<PatchSetApproval> del = Lists.newArrayList(); List<PatchSetApproval> del = Lists.newArrayList();
List<PatchSetApproval> ups = Lists.newArrayList(); List<PatchSetApproval> ups = Lists.newArrayList();
Map<String, PatchSetApproval> current = scanLabels(ctx, del); Map<String, PatchSetApproval> current = scanLabels(ctx, del);
// get all approvals in cases of quick approve vote
Map<String, Short> allApprovals = Collections.emptyMap();
if (current != null) {
allApprovals = approvalsByKey(current.values());
}
allApprovals.putAll(inLabels);
// get previous label votes
Map<String, Short> currentLabels = new HashMap<>();
for (Map.Entry<String, PatchSetApproval> ent : current.entrySet()) {
currentLabels.put(ent.getValue().getLabel(), ent.getValue().getValue());
}
Map<String, Short> previous = new HashMap<>();
for (Map.Entry<String, Short> ent : allApprovals.entrySet()) {
if (!currentLabels.containsKey(ent.getKey())) {
previous.put(ent.getKey(), (short)0);
} else {
previous.put(ent.getKey(), currentLabels.get(ent.getKey()));
}
}
ChangeUpdate update = ctx.getUpdate(psId); ChangeUpdate update = ctx.getUpdate(psId);
LabelTypes labelTypes = ctx.getControl().getLabelTypes(); LabelTypes labelTypes = ctx.getControl().getLabelTypes();
for (Map.Entry<String, Short> ent : labels.entrySet()) { for (Map.Entry<String, Short> ent : allApprovals.entrySet()) {
String name = ent.getKey(); String name = ent.getKey();
LabelType lt = checkNotNull(labelTypes.byLabel(name), name); LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
PatchSetApproval c = current.remove(lt.getName()); PatchSetApproval c = current.remove(lt.getName());
String normName = lt.getName(); String normName = lt.getName();
approvals.put(normName, (short) 0);
if (ent.getValue() == null || ent.getValue() == 0) { if (ent.getValue() == null || ent.getValue() == 0) {
// User requested delete of this label. // User requested delete of this label.
oldApprovals.put(normName, null);
if (c != null) { if (c != null) {
if (c.getValue() != 0) { if (c.getValue() != 0) {
addLabelDelta(normName, (short) 0); addLabelDelta(normName, (short) 0);
oldApprovals.put(normName, previous.get(normName));
} }
del.add(c); del.add(c);
update.putApproval(ent.getKey(), (short) 0); update.putApproval(normName, (short) 0);
} }
} else if (c != null && c.getValue() != ent.getValue()) { } else if (c != null && c.getValue() != ent.getValue()) {
c.setValue(ent.getValue()); c.setValue(ent.getValue());
c.setGranted(ctx.getWhen()); c.setGranted(ctx.getWhen());
ups.add(c); ups.add(c);
addLabelDelta(normName, c.getValue()); addLabelDelta(normName, c.getValue());
categories.put(normName, c.getValue()); oldApprovals.put(normName, previous.get(normName));
update.putApproval(ent.getKey(), ent.getValue()); approvals.put(normName, c.getValue());
update.putApproval(normName, ent.getValue());
} else if (c != null && c.getValue() == ent.getValue()) { } else if (c != null && c.getValue() == ent.getValue()) {
current.put(normName, c); current.put(normName, c);
oldApprovals.put(normName, null);
approvals.put(normName, c.getValue());
} else if (c == null) { } else if (c == null) {
c = new PatchSetApproval(new PatchSetApproval.Key( c = new PatchSetApproval(new PatchSetApproval.Key(
psId, psId,
@@ -560,9 +597,10 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
c.setGranted(ctx.getWhen()); c.setGranted(ctx.getWhen());
ups.add(c); ups.add(c);
addLabelDelta(normName, c.getValue()); addLabelDelta(normName, c.getValue());
categories.put(normName, c.getValue()); oldApprovals.put(normName, previous.get(normName));
approvals.put(normName, c.getValue());
update.putReviewer(user.getAccountId(), REVIEWER); update.putReviewer(user.getAccountId(), REVIEWER);
update.putApproval(ent.getKey(), ent.getValue()); update.putApproval(normName, ent.getValue());
} }
} }

View File

@@ -18,7 +18,7 @@ public class ApprovalAttribute {
public String type; public String type;
public String description; public String description;
public String value; public String value;
public String oldValue;
public Long grantedOn; public Long grantedOn;
public AccountAttribute by; public AccountAttribute by;
} }

View File

@@ -620,6 +620,7 @@ public class EventFactory {
a.value = Short.toString(approval.getValue()); a.value = Short.toString(approval.getValue());
a.by = asAccountAttribute(approval.getAccountId()); a.by = asAccountAttribute(approval.getAccountId());
a.grantedOn = approval.getGranted().getTime() / 1000L; a.grantedOn = approval.getGranted().getTime() / 1000L;
a.oldValue = null;
LabelType lt = labelTypes.byLabel(approval.getLabelId()); LabelType lt = labelTypes.byLabel(approval.getLabelId());
if (lt != null) { if (lt != null) {

View File

@@ -45,6 +45,8 @@ import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.mail.MergedSender; import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender; import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.LabelVote;
@@ -93,6 +95,7 @@ public class ReplaceOp extends BatchUpdate.Op {
private final PatchSetUtil psUtil; private final PatchSetUtil psUtil;
private final ChangeData.Factory changeDataFactory; private final ChangeData.Factory changeDataFactory;
private final ChangeControl.GenericFactory changeControlFactory;
private final ChangeKindCache changeKindCache; private final ChangeKindCache changeKindCache;
private final ChangeMessagesUtil cmUtil; private final ChangeMessagesUtil cmUtil;
private final ChangeHooks hooks; private final ChangeHooks hooks;
@@ -127,6 +130,7 @@ public class ReplaceOp extends BatchUpdate.Op {
@AssistedInject @AssistedInject
ReplaceOp(PatchSetUtil psUtil, ReplaceOp(PatchSetUtil psUtil,
ChangeData.Factory changeDataFactory, ChangeData.Factory changeDataFactory,
ChangeControl.GenericFactory changeControlFactory,
ChangeKindCache changeKindCache, ChangeKindCache changeKindCache,
ChangeMessagesUtil cmUtil, ChangeMessagesUtil cmUtil,
ChangeHooks hooks, ChangeHooks hooks,
@@ -149,6 +153,7 @@ public class ReplaceOp extends BatchUpdate.Op {
@Assisted @Nullable PushCertificate pushCertificate) { @Assisted @Nullable PushCertificate pushCertificate) {
this.psUtil = psUtil; this.psUtil = psUtil;
this.changeDataFactory = changeDataFactory; this.changeDataFactory = changeDataFactory;
this.changeControlFactory = changeControlFactory;
this.changeKindCache = changeKindCache; this.changeKindCache = changeKindCache;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.hooks = hooks; this.hooks = hooks;
@@ -391,12 +396,45 @@ public class ReplaceOp extends BatchUpdate.Op {
hooks.doChangeMergedHook(change, account, newPatchSet, ctx.getDb(), hooks.doChangeMergedHook(change, account, newPatchSet, ctx.getDb(),
commit.getName()); commit.getName());
} }
if (!approvals.isEmpty()) { try{
hooks.doCommentAddedHook(change, account, newPatchSet, null, approvals, runHook(ctx);
ctx.getDb()); } catch (Exception e) {
log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
} }
} }
private void runHook(final Context ctx) throws NoSuchChangeException,
OrmException {
if (approvals.isEmpty()) {
return;
}
/** For labels that are not set in this operation, show the "current" value
* of 0, and no oldValue as the value was not modified by this operation.
* For labels that are set in this operation, the value was modified, so
* show a transition from an oldValue of 0 to the new value.
**/
ChangeControl changeControl = changeControlFactory.controlFor(
ctx.getDb(), change, ctx.getUser());
List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
Map<String, Short> allApprovals = new HashMap<>();
Map<String, Short> oldApprovals = new HashMap<>();
for (LabelType lt : labels){
allApprovals.put(lt.getName(), (short) 0);
oldApprovals.put(lt.getName(), null);
}
for (Map.Entry<String, Short> entry : approvals.entrySet()) {
if (entry.getValue() != 0) {
allApprovals.put(entry.getKey(), entry.getValue());
oldApprovals.put(entry.getKey(), (short) 0);
}
}
hooks.doCommentAddedHook(change,
ctx.getUser().asIdentifiedUser().getAccount(), newPatchSet, null,
allApprovals, oldApprovals, ctx.getDb());
}
private void sendMergedEmail(final Context ctx) { private void sendMergedEmail(final Context ctx) {
sendEmailExecutor.submit(requestScopePropagator.wrap(new Runnable() { sendEmailExecutor.submit(requestScopePropagator.wrap(new Runnable() {
@Override @Override