AssigneeChanged event

Fired when a change's assignee has been changed or removed:

  type: "assignee-changed"
  change: The updated change
  changer: The user that issued the assignment
  oldAssignee: Assignee before it was changed.
  eventCreatedOn: Timestamp describing when this event was created.

Change-Id: I5bf1f3f71b84afda82ecb1058a9cbf04185474f3
This commit is contained in:
Gustaf Lundh 2016-09-20 17:17:41 +02:00
parent 0e907f9812
commit 27b133bea8
11 changed files with 223 additions and 13 deletions

View File

@ -1,5 +1,4 @@
= gerrit stream-events
== NAME
gerrit stream-events - Monitor events occurring in real time
@ -59,6 +58,21 @@ this JSON stream should deal with that appropriately.
[[events]]
== EVENTS
=== Assignee Changed
Sent when the assignee of a change has been modified.
type:: "assignee-changed"
change:: link:json.html#change[change attribute]
changer:: link:json.html#account[account attribute]
oldAssignee:: Assignee before it was changed.
eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
created.
=== Change Abandoned
Sent when a change has been abandoned.

View File

@ -0,0 +1,29 @@
// 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.extensions.events;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.extensions.common.AccountInfo;
/** Notified whenever a change assignee is changed. */
@ExtensionPoint
public interface AssigneeChangedListener {
interface Event extends ChangeEvent {
@Nullable AccountInfo getOldAssignee();
}
void onAssigneeChanged(Event event);
}

View File

@ -30,8 +30,10 @@ import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountJson;
import com.google.gerrit.server.change.DeleteAssignee.Input;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.extensions.events.AssigneeChanged;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gwtorm.server.OrmException;
@ -48,6 +50,7 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
private final ChangeMessagesUtil cmUtil;
private final Provider<ReviewDb> db;
private final AccountInfoCacheFactory.Factory accountInfos;
private final AssigneeChanged assigneeChanged;
private final String anonymousCowardName;
@Inject
@ -55,11 +58,13 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
ChangeMessagesUtil cmUtil,
Provider<ReviewDb> db,
AccountInfoCacheFactory.Factory accountInfosFactory,
AssigneeChanged assigneeChanged,
@AnonymousCowardName String anonymousCowardName) {
this.batchUpdateFactory = batchUpdateFactory;
this.cmUtil = cmUtil;
this.db = db;
this.accountInfos = accountInfosFactory;
this.assigneeChanged = assigneeChanged;
this.anonymousCowardName = anonymousCowardName;
}
@ -80,6 +85,7 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
}
private class Op extends BatchUpdate.Op {
private Change change;
private Account deletedAssignee;
@Override
@ -88,19 +94,18 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
if (!ctx.getControl().canEditAssignee()) {
throw new AuthException("Delete Assignee not permitted");
}
Change change = ctx.getChange();
change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
Account.Id currentAssigneeId = change.getAssignee();
if (currentAssigneeId == null) {
return false;
}
Account account = accountInfos.create().get(currentAssigneeId);
deletedAssignee = accountInfos.create().get(currentAssigneeId);
// noteDb
update.removeAssignee();
// reviewDb
change.setAssignee(null);
addMessage(ctx, update, account);
deletedAssignee = account;
addMessage(ctx, update, deletedAssignee);
return true;
}
@ -120,5 +125,10 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
"Assignee deleted: " + deleted.getName(anonymousCowardName));
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
}
@Override
public void postUpdate(Context ctx) throws OrmException {
assigneeChanged.fire(change, ctx.getAccount(), deletedAssignee, ctx.getWhen());
}
}
}

View File

@ -30,7 +30,9 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.extensions.events.AssigneeChanged;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.validators.AssigneeValidationListener;
import com.google.gerrit.server.validators.ValidationException;
@ -49,20 +51,25 @@ public class SetAssigneeOp extends BatchUpdate.Op {
private final DynamicSet<AssigneeValidationListener> validationListeners;
private final AssigneeInput input;
private final String anonymousCowardName;
private final AssigneeChanged assigneeChanged;
private Change change;
private Account newAssignee;
private Account oldAssignee;
@AssistedInject
SetAssigneeOp(AccountsCollection accounts,
ChangeMessagesUtil cmUtil,
AccountInfoCacheFactory.Factory accountInfosFactory,
DynamicSet<AssigneeValidationListener> validationListeners,
AssigneeChanged assigneeChanged,
@AnonymousCowardName String anonymousCowardName,
@Assisted AssigneeInput input) {
this.accounts = accounts;
this.cmUtil = cmUtil;
this.accountInfosFactory = accountInfosFactory;
this.validationListeners = validationListeners;
this.assigneeChanged = assigneeChanged;
this.anonymousCowardName = anonymousCowardName;
this.input = input;
}
@ -73,17 +80,17 @@ public class SetAssigneeOp extends BatchUpdate.Op {
if (!ctx.getControl().canEditAssignee()) {
throw new AuthException("Changing Assignee not permitted");
}
Change change = ctx.getChange();
change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
Optional<Account.Id> oldAssigneeId =
Optional.fromNullable(ctx.getChange().getAssignee());
Optional.fromNullable(change.getAssignee());
if (input.assignee == null) {
if (oldAssigneeId.isPresent()) {
throw new BadRequestException("Cannot set Assignee to empty");
}
return false;
}
Account oldAssignee = null;
oldAssignee = null;
if (oldAssigneeId.isPresent()) {
oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get());
}
@ -100,7 +107,7 @@ public class SetAssigneeOp extends BatchUpdate.Op {
if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) {
throw new AuthException(String.format(
"Change %s is not visible to %s.",
ctx.getChange().getChangeId(),
change.getChangeId(),
newAssigneeUser.getUserName()));
}
try {
@ -134,14 +141,19 @@ public class SetAssigneeOp extends BatchUpdate.Op {
}
ChangeMessage cmsg = new ChangeMessage(
new ChangeMessage.Key(
ctx.getChange().getId(),
change.getId(),
ChangeUtil.messageUUID(ctx.getDb())),
ctx.getAccountId(), ctx.getWhen(),
ctx.getChange().currentPatchSetId());
change.currentPatchSetId());
cmsg.setMessage(msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
}
@Override
public void postUpdate(Context ctx) throws OrmException {
assigneeChanged.fire(change, ctx.getAccount(), oldAssignee, ctx.getWhen());
}
public Account getNewAssignee() {
return newAssignee;
}

View File

@ -30,6 +30,7 @@ import com.google.gerrit.extensions.config.DownloadScheme;
import com.google.gerrit.extensions.config.ExternalIncludedIn;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
@ -306,6 +307,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), CacheRemovalListener.class);
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), AssigneeChangedListener.class);
DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
DynamicSet.setOf(binder(), CommentAddedListener.class);
DynamicSet.setOf(binder(), DraftPublishedListener.class);

View File

@ -26,6 +26,7 @@ public class ChangeAttribute {
public String number;
public String subject;
public AccountAttribute owner;
public AccountAttribute assignee;
public String url;
public String commitMessage;

View File

@ -0,0 +1,29 @@
// 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.server.events;
import com.google.common.base.Supplier;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.data.AccountAttribute;
public class AssigneeChangedEvent extends ChangeEvent {
static final String TYPE = "assignee-changed";
public Supplier<AccountAttribute> changer;
public Supplier<AccountAttribute> oldAssignee;
public AssigneeChangedEvent(Change change) {
super(TYPE, change);
}
}

View File

@ -158,6 +158,7 @@ public class EventFactory {
}
a.url = getChangeUrl(change);
a.owner = asAccountAttribute(change.getOwner());
a.assignee = asAccountAttribute(change.getAssignee());
a.status = change.getStatus();
return a;
}

View File

@ -22,6 +22,7 @@ public class EventTypes {
private static final Map<String, Class<?>> typesByString = new HashMap<>();
static {
register(AssigneeChangedEvent.TYPE, AssigneeChangedEvent.class);
register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class);
register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class);
register(ChangeRestoredEvent.TYPE, ChangeRestoredEvent.class);

View File

@ -24,6 +24,7 @@ import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
@ -73,6 +74,7 @@ import java.util.Map.Entry;
@Singleton
public class StreamEventsApiListener implements
AssigneeChangedListener,
ChangeAbandonedListener,
ChangeMergedListener,
ChangeRestoredListener,
@ -91,6 +93,8 @@ public class StreamEventsApiListener implements
public static class Module extends LifecycleModule {
@Override
protected void configure() {
DynamicSet.bind(binder(), AssigneeChangedListener.class)
.to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeAbandonedListener.class)
.to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeMergedListener.class)
@ -177,8 +181,8 @@ public class StreamEventsApiListener implements
new Supplier<AccountAttribute>() {
@Override
public AccountAttribute get() {
return eventFactory.asAccountAttribute(
new Account.Id(account._accountId));
return account != null ? eventFactory.asAccountAttribute(
new Account.Id(account._accountId)) : null;
}
});
}
@ -265,6 +269,22 @@ public class StreamEventsApiListener implements
return null;
}
@Override
public void onAssigneeChanged(AssigneeChangedListener.Event ev) {
try {
Change change = getChange(ev.getChange());
AssigneeChangedEvent event = new AssigneeChangedEvent(change);
event.change = changeAttributeSupplier(change);
event.changer = accountAttributeSupplier(ev.getWho());
event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
dispatcher.get().postEvent(change, event);
} catch (OrmException e) {
log.error("Failed to dispatch event", e);
}
}
@Override
public void onTopicEdited(TopicEditedListener.Event ev) {
try {

View File

@ -0,0 +1,91 @@
// 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.server.extensions.events;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Timestamp;
public class AssigneeChanged {
private static final Logger log =
LoggerFactory.getLogger(AssigneeChanged.class);
private final DynamicSet<AssigneeChangedListener> listeners;
private final EventUtil util;
@Inject
AssigneeChanged(DynamicSet<AssigneeChangedListener> listeners,
EventUtil util) {
this.listeners = listeners;
this.util = util;
}
public void fire(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee,
Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
Event event = new Event(change, editor, oldAssignee, when);
for (AssigneeChangedListener l : listeners) {
try {
l.onAssigneeChanged(event);
} catch (Exception e) {
log.warn("Error in event listener", e);
}
}
}
public void fire(Change change, Account account, Account oldAssignee,
Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
fire(util.changeInfo(change),
util.accountInfo(account),
util.accountInfo(oldAssignee),
when);
} catch (OrmException e) {
log.error("Couldn't fire event", e);
}
}
private static class Event extends AbstractChangeEvent
implements AssigneeChangedListener.Event {
private final AccountInfo oldAssignee;
Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee,
Timestamp when) {
super(change, editor, when, NotifyHandling.ALL);
this.oldAssignee = oldAssignee;
}
@Override
public AccountInfo getOldAssignee() {
return oldAssignee;
}
}
}