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 = gerrit stream-events
== NAME == NAME
gerrit stream-events - Monitor events occurring in real time gerrit stream-events - Monitor events occurring in real time
@ -59,6 +58,21 @@ this JSON stream should deal with that appropriately.
[[events]] [[events]]
== 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 === Change Abandoned
Sent when a change has been 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.account.AccountJson;
import com.google.gerrit.server.change.DeleteAssignee.Input; import com.google.gerrit.server.change.DeleteAssignee.Input;
import com.google.gerrit.server.config.AnonymousCowardName; 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;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext; 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.git.UpdateException;
import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@ -48,6 +50,7 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
private final ChangeMessagesUtil cmUtil; private final ChangeMessagesUtil cmUtil;
private final Provider<ReviewDb> db; private final Provider<ReviewDb> db;
private final AccountInfoCacheFactory.Factory accountInfos; private final AccountInfoCacheFactory.Factory accountInfos;
private final AssigneeChanged assigneeChanged;
private final String anonymousCowardName; private final String anonymousCowardName;
@Inject @Inject
@ -55,11 +58,13 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
ChangeMessagesUtil cmUtil, ChangeMessagesUtil cmUtil,
Provider<ReviewDb> db, Provider<ReviewDb> db,
AccountInfoCacheFactory.Factory accountInfosFactory, AccountInfoCacheFactory.Factory accountInfosFactory,
AssigneeChanged assigneeChanged,
@AnonymousCowardName String anonymousCowardName) { @AnonymousCowardName String anonymousCowardName) {
this.batchUpdateFactory = batchUpdateFactory; this.batchUpdateFactory = batchUpdateFactory;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.db = db; this.db = db;
this.accountInfos = accountInfosFactory; this.accountInfos = accountInfosFactory;
this.assigneeChanged = assigneeChanged;
this.anonymousCowardName = anonymousCowardName; this.anonymousCowardName = anonymousCowardName;
} }
@ -80,6 +85,7 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
} }
private class Op extends BatchUpdate.Op { private class Op extends BatchUpdate.Op {
private Change change;
private Account deletedAssignee; private Account deletedAssignee;
@Override @Override
@ -88,19 +94,18 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
if (!ctx.getControl().canEditAssignee()) { if (!ctx.getControl().canEditAssignee()) {
throw new AuthException("Delete Assignee not permitted"); throw new AuthException("Delete Assignee not permitted");
} }
Change change = ctx.getChange(); change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
Account.Id currentAssigneeId = change.getAssignee(); Account.Id currentAssigneeId = change.getAssignee();
if (currentAssigneeId == null) { if (currentAssigneeId == null) {
return false; return false;
} }
Account account = accountInfos.create().get(currentAssigneeId); deletedAssignee = accountInfos.create().get(currentAssigneeId);
// noteDb // noteDb
update.removeAssignee(); update.removeAssignee();
// reviewDb // reviewDb
change.setAssignee(null); change.setAssignee(null);
addMessage(ctx, update, account); addMessage(ctx, update, deletedAssignee);
deletedAssignee = account;
return true; return true;
} }
@ -120,5 +125,10 @@ public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
"Assignee deleted: " + deleted.getName(anonymousCowardName)); "Assignee deleted: " + deleted.getName(anonymousCowardName));
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); 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.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.config.AnonymousCowardName; 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;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.validators.AssigneeValidationListener; import com.google.gerrit.server.validators.AssigneeValidationListener;
import com.google.gerrit.server.validators.ValidationException; import com.google.gerrit.server.validators.ValidationException;
@ -49,20 +51,25 @@ public class SetAssigneeOp extends BatchUpdate.Op {
private final DynamicSet<AssigneeValidationListener> validationListeners; private final DynamicSet<AssigneeValidationListener> validationListeners;
private final AssigneeInput input; private final AssigneeInput input;
private final String anonymousCowardName; private final String anonymousCowardName;
private final AssigneeChanged assigneeChanged;
private Change change;
private Account newAssignee; private Account newAssignee;
private Account oldAssignee;
@AssistedInject @AssistedInject
SetAssigneeOp(AccountsCollection accounts, SetAssigneeOp(AccountsCollection accounts,
ChangeMessagesUtil cmUtil, ChangeMessagesUtil cmUtil,
AccountInfoCacheFactory.Factory accountInfosFactory, AccountInfoCacheFactory.Factory accountInfosFactory,
DynamicSet<AssigneeValidationListener> validationListeners, DynamicSet<AssigneeValidationListener> validationListeners,
AssigneeChanged assigneeChanged,
@AnonymousCowardName String anonymousCowardName, @AnonymousCowardName String anonymousCowardName,
@Assisted AssigneeInput input) { @Assisted AssigneeInput input) {
this.accounts = accounts; this.accounts = accounts;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.accountInfosFactory = accountInfosFactory; this.accountInfosFactory = accountInfosFactory;
this.validationListeners = validationListeners; this.validationListeners = validationListeners;
this.assigneeChanged = assigneeChanged;
this.anonymousCowardName = anonymousCowardName; this.anonymousCowardName = anonymousCowardName;
this.input = input; this.input = input;
} }
@ -73,17 +80,17 @@ public class SetAssigneeOp extends BatchUpdate.Op {
if (!ctx.getControl().canEditAssignee()) { if (!ctx.getControl().canEditAssignee()) {
throw new AuthException("Changing Assignee not permitted"); throw new AuthException("Changing Assignee not permitted");
} }
Change change = ctx.getChange(); change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
Optional<Account.Id> oldAssigneeId = Optional<Account.Id> oldAssigneeId =
Optional.fromNullable(ctx.getChange().getAssignee()); Optional.fromNullable(change.getAssignee());
if (input.assignee == null) { if (input.assignee == null) {
if (oldAssigneeId.isPresent()) { if (oldAssigneeId.isPresent()) {
throw new BadRequestException("Cannot set Assignee to empty"); throw new BadRequestException("Cannot set Assignee to empty");
} }
return false; return false;
} }
Account oldAssignee = null; oldAssignee = null;
if (oldAssigneeId.isPresent()) { if (oldAssigneeId.isPresent()) {
oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get()); oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get());
} }
@ -100,7 +107,7 @@ public class SetAssigneeOp extends BatchUpdate.Op {
if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) { if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) {
throw new AuthException(String.format( throw new AuthException(String.format(
"Change %s is not visible to %s.", "Change %s is not visible to %s.",
ctx.getChange().getChangeId(), change.getChangeId(),
newAssigneeUser.getUserName())); newAssigneeUser.getUserName()));
} }
try { try {
@ -134,14 +141,19 @@ public class SetAssigneeOp extends BatchUpdate.Op {
} }
ChangeMessage cmsg = new ChangeMessage( ChangeMessage cmsg = new ChangeMessage(
new ChangeMessage.Key( new ChangeMessage.Key(
ctx.getChange().getId(), change.getId(),
ChangeUtil.messageUUID(ctx.getDb())), ChangeUtil.messageUUID(ctx.getDb())),
ctx.getAccountId(), ctx.getWhen(), ctx.getAccountId(), ctx.getWhen(),
ctx.getChange().currentPatchSetId()); change.currentPatchSetId());
cmsg.setMessage(msg.toString()); cmsg.setMessage(msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); 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() { public Account getNewAssignee() {
return newAssignee; 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.ExternalIncludedIn;
import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.events.AgreementSignupListener; 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.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener; import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener; import com.google.gerrit.extensions.events.ChangeMergedListener;
@ -306,6 +307,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), CacheRemovalListener.class); DynamicSet.setOf(binder(), CacheRemovalListener.class);
DynamicMap.mapOf(binder(), CapabilityDefinition.class); DynamicMap.mapOf(binder(), CapabilityDefinition.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class); DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), AssigneeChangedListener.class);
DynamicSet.setOf(binder(), ChangeAbandonedListener.class); DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
DynamicSet.setOf(binder(), CommentAddedListener.class); DynamicSet.setOf(binder(), CommentAddedListener.class);
DynamicSet.setOf(binder(), DraftPublishedListener.class); DynamicSet.setOf(binder(), DraftPublishedListener.class);

View File

@ -26,6 +26,7 @@ public class ChangeAttribute {
public String number; public String number;
public String subject; public String subject;
public AccountAttribute owner; public AccountAttribute owner;
public AccountAttribute assignee;
public String url; public String url;
public String commitMessage; 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.url = getChangeUrl(change);
a.owner = asAccountAttribute(change.getOwner()); a.owner = asAccountAttribute(change.getOwner());
a.assignee = asAccountAttribute(change.getAssignee());
a.status = change.getStatus(); a.status = change.getStatus();
return a; return a;
} }

View File

@ -22,6 +22,7 @@ public class EventTypes {
private static final Map<String, Class<?>> typesByString = new HashMap<>(); private static final Map<String, Class<?>> typesByString = new HashMap<>();
static { static {
register(AssigneeChangedEvent.TYPE, AssigneeChangedEvent.class);
register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class); register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class);
register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class); register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class);
register(ChangeRestoredEvent.TYPE, ChangeRestoredEvent.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.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevisionInfo; 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.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener; import com.google.gerrit.extensions.events.ChangeMergedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener; import com.google.gerrit.extensions.events.ChangeRestoredListener;
@ -73,6 +74,7 @@ import java.util.Map.Entry;
@Singleton @Singleton
public class StreamEventsApiListener implements public class StreamEventsApiListener implements
AssigneeChangedListener,
ChangeAbandonedListener, ChangeAbandonedListener,
ChangeMergedListener, ChangeMergedListener,
ChangeRestoredListener, ChangeRestoredListener,
@ -91,6 +93,8 @@ public class StreamEventsApiListener implements
public static class Module extends LifecycleModule { public static class Module extends LifecycleModule {
@Override @Override
protected void configure() { protected void configure() {
DynamicSet.bind(binder(), AssigneeChangedListener.class)
.to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeAbandonedListener.class) DynamicSet.bind(binder(), ChangeAbandonedListener.class)
.to(StreamEventsApiListener.class); .to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeMergedListener.class) DynamicSet.bind(binder(), ChangeMergedListener.class)
@ -177,8 +181,8 @@ public class StreamEventsApiListener implements
new Supplier<AccountAttribute>() { new Supplier<AccountAttribute>() {
@Override @Override
public AccountAttribute get() { public AccountAttribute get() {
return eventFactory.asAccountAttribute( return account != null ? eventFactory.asAccountAttribute(
new Account.Id(account._accountId)); new Account.Id(account._accountId)) : null;
} }
}); });
} }
@ -265,6 +269,22 @@ public class StreamEventsApiListener implements
return null; 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 @Override
public void onTopicEdited(TopicEditedListener.Event ev) { public void onTopicEdited(TopicEditedListener.Event ev) {
try { 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;
}
}
}