BatchUpdate: Add a combined operation spanning all phases
A common pattern thus far has been to communicate between ChangeOps and subsequent Callables using AtomicReferences in the caller. This works, but using so many AtomicReferences smells kind of wrong, particularly since the caller-provided portions of BatchUpdate are single-threaded. Instead, create a single class Op, which may have steps for each of the several phases, including a new repo-updating phase. Each of these steps is passed in a Context object, which includes a phase-specific view of the BatchUpdate. This encapsulation prevents operations from doing unsupported things like adding new operations in the middle of the execute process. Ops can now communicate between different phases or with the caller using instance variables. Change-Id: Id97dbb772d2e3051d6a74bb8e819d691843a45e1
This commit is contained in:
@@ -19,7 +19,6 @@ import com.google.gerrit.common.ChangeHooks;
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.DefaultInput;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.Response;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||
@@ -32,9 +31,9 @@ import com.google.gerrit.server.ChangeUtil;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.change.PutTopic.Input;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.ChangeOp;
|
||||
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.gerrit.server.project.ChangeControl;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
@@ -42,10 +41,7 @@ import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Singleton
|
||||
public class PutTopic implements RestModifyView<ChangeResource, Input>,
|
||||
@@ -75,73 +71,73 @@ public class PutTopic implements RestModifyView<ChangeResource, Input>,
|
||||
public Response<String> apply(ChangeResource req, Input input)
|
||||
throws AuthException, UpdateException, RestApiException, OrmException,
|
||||
IOException {
|
||||
if (input == null) {
|
||||
input = new Input();
|
||||
}
|
||||
final String inputTopic = input.topic;
|
||||
|
||||
ChangeControl control = req.getControl();
|
||||
if (!control.canEditTopicName()) {
|
||||
ChangeControl ctl = req.getControl();
|
||||
if (!ctl.canEditTopicName()) {
|
||||
throw new AuthException("changing topic not permitted");
|
||||
}
|
||||
|
||||
final Change.Id id = req.getChange().getId();
|
||||
final IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
|
||||
final AtomicReference<Change> change = new AtomicReference<>();
|
||||
final AtomicReference<String> oldTopicName = new AtomicReference<>();
|
||||
final AtomicReference<String> newTopicName = new AtomicReference<>();
|
||||
|
||||
final Timestamp now = TimeUtil.nowTs();
|
||||
Op op = new Op(ctl, input != null ? input : new Input());
|
||||
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
|
||||
req.getChange().getProject(), now)) {
|
||||
u.addChangeOp(new ChangeOp(req.getControl()) {
|
||||
@Override
|
||||
public void call(ReviewDb db, ChangeUpdate update) throws OrmException,
|
||||
ResourceConflictException {
|
||||
Change c = db.changes().get(id);
|
||||
String n = Strings.nullToEmpty(inputTopic);
|
||||
String o = Strings.nullToEmpty(c.getTopic());
|
||||
if (o.equals(n)) {
|
||||
return;
|
||||
}
|
||||
String summary;
|
||||
if (o.isEmpty()) {
|
||||
summary = "Topic set to " + n;
|
||||
} else if (n.isEmpty()) {
|
||||
summary = "Topic " + o + " removed";
|
||||
} else {
|
||||
summary = String.format("Topic changed from %s to %s", o, n);
|
||||
}
|
||||
c.setTopic(Strings.emptyToNull(n));
|
||||
ChangeUtil.updated(c);
|
||||
db.changes().update(Collections.singleton(c));
|
||||
|
||||
ChangeMessage cmsg = new ChangeMessage(
|
||||
new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)),
|
||||
caller.getAccountId(), now, c.currentPatchSetId());
|
||||
cmsg.setMessage(summary);
|
||||
cmUtil.addChangeMessage(db, update, cmsg);
|
||||
|
||||
change.set(c);
|
||||
oldTopicName.set(o);
|
||||
newTopicName.set(n);
|
||||
}
|
||||
});
|
||||
u.addPostOp(new Callable<Void>() {
|
||||
@Override
|
||||
public Void call() throws OrmException {
|
||||
Change c = change.get();
|
||||
if (c != null) {
|
||||
hooks.doTopicChangedHook(change.get(), caller.getAccount(),
|
||||
oldTopicName.get(), dbProvider.get());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
req.getChange().getProject(), TimeUtil.nowTs())) {
|
||||
u.addOp(ctl, op);
|
||||
u.execute();
|
||||
}
|
||||
String n = newTopicName.get();
|
||||
return Strings.isNullOrEmpty(n) ? Response.<String> none() : Response.ok(n);
|
||||
return Strings.isNullOrEmpty(op.newTopicName)
|
||||
? Response.<String> none()
|
||||
: Response.ok(op.newTopicName);
|
||||
}
|
||||
|
||||
private class Op extends BatchUpdate.Op {
|
||||
private final Input input;
|
||||
private final IdentifiedUser caller;
|
||||
|
||||
private Change change;
|
||||
private String oldTopicName;
|
||||
private String newTopicName;
|
||||
|
||||
public Op(ChangeControl ctl, Input input) {
|
||||
this.input = input;
|
||||
this.caller = (IdentifiedUser) ctl.getCurrentUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateChange(ChangeContext ctx) throws OrmException {
|
||||
change = ctx.readChange();
|
||||
String newTopicName = Strings.nullToEmpty(input.topic);
|
||||
String oldTopicName = Strings.nullToEmpty(change.getTopic());
|
||||
if (oldTopicName.equals(newTopicName)) {
|
||||
return;
|
||||
}
|
||||
String summary;
|
||||
if (oldTopicName.isEmpty()) {
|
||||
summary = "Topic set to " + newTopicName;
|
||||
} else if (newTopicName.isEmpty()) {
|
||||
summary = "Topic " + oldTopicName + " removed";
|
||||
} else {
|
||||
summary = String.format("Topic changed from %s to %s",
|
||||
oldTopicName, newTopicName);
|
||||
}
|
||||
change.setTopic(Strings.emptyToNull(newTopicName));
|
||||
ChangeUtil.updated(change);
|
||||
ctx.getDb().changes().update(Collections.singleton(change));
|
||||
|
||||
ChangeMessage cmsg = new ChangeMessage(
|
||||
new ChangeMessage.Key(
|
||||
change.getId(),
|
||||
ChangeUtil.messageUUID(ctx.getDb())),
|
||||
caller.getAccountId(), ctx.getWhen(),
|
||||
change.currentPatchSetId());
|
||||
cmsg.setMessage(summary);
|
||||
cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), cmsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postUpdate(Context ctx) throws OrmException {
|
||||
if (change != null) {
|
||||
hooks.doTopicChangedHook(change, caller.getAccount(),
|
||||
oldTopicName, ctx.getDb());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user