Merge "Split comments and new PS info with new PS upload"

This commit is contained in:
Youssef Elghareeb
2020-01-10 09:13:44 +00:00
committed by Gerrit Code Review
10 changed files with 281 additions and 104 deletions

View File

@@ -28,6 +28,7 @@ import com.google.gerrit.extensions.validators.CommentForValidation;
import com.google.gerrit.extensions.validators.CommentValidationFailure; import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator; import com.google.gerrit.extensions.validators.CommentValidator;
import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.plugincontext.PluginSetContext; import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -57,7 +58,7 @@ public class PublishCommentUtil {
public void publish( public void publish(
ChangeContext ctx, ChangeContext ctx,
PatchSet.Id psId, ChangeUpdate changeUpdate,
Collection<Comment> draftComments, Collection<Comment> draftComments,
@Nullable String tag) { @Nullable String tag) {
ChangeNotes notes = ctx.getNotes(); ChangeNotes notes = ctx.getNotes();
@@ -107,7 +108,7 @@ public class PublishCommentUtil {
} }
commentsToPublish.add(draftComment); commentsToPublish.add(draftComment);
} }
commentsUtil.putComments(ctx.getUpdate(psId), Status.PUBLISHED, commentsToPublish); commentsUtil.putComments(changeUpdate, Status.PUBLISHED, commentsToPublish);
} }
private static PatchSet.Id psId(ChangeNotes notes, Comment c) { private static PatchSet.Id psId(ChangeNotes notes, Comment c) {

View File

@@ -0,0 +1,151 @@
// Copyright (C) 2020 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;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.CommentsRejectedException;
import com.google.gerrit.server.update.Context;
import com.google.gerrit.server.util.LabelVote;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link BatchUpdateOp} that can be used to publish draft comments
*
* <p>This class uses the {@link PublishCommentUtil} to publish draft comments and fires the
* necessary event for this.
*/
public class PublishCommentsOp implements BatchUpdateOp {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final PatchSetUtil psUtil;
private final ChangeNotes.Factory changeNotesFactory;
private final ChangeMessagesUtil cmUtil;
private final CommentAdded commentAdded;
private final CommentsUtil commentsUtil;
private final EmailReviewComments.Factory email;
private final List<LabelVote> labelDelta = new ArrayList<>();
private final Project.NameKey projectNameKey;
private final PatchSet.Id psId;
private final PublishCommentUtil publishCommentUtil;
private List<Comment> comments = new ArrayList<>();
private ChangeMessage message;
private IdentifiedUser user;
public interface Factory {
PublishCommentsOp create(PatchSet.Id psId, Project.NameKey projectNameKey);
}
@Inject
public PublishCommentsOp(
ChangeNotes.Factory changeNotesFactory,
ChangeMessagesUtil cmUtil,
CommentAdded commentAdded,
CommentsUtil commentsUtil,
EmailReviewComments.Factory email,
PatchSetUtil psUtil,
PublishCommentUtil publishCommentUtil,
@Assisted PatchSet.Id psId,
@Assisted Project.NameKey projectNameKey) {
this.cmUtil = cmUtil;
this.changeNotesFactory = changeNotesFactory;
this.commentAdded = commentAdded;
this.commentsUtil = commentsUtil;
this.email = email;
this.psId = psId;
this.publishCommentUtil = publishCommentUtil;
this.psUtil = psUtil;
this.projectNameKey = projectNameKey;
}
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceConflictException, UnprocessableEntityException, IOException,
PatchListNotAvailableException, CommentsRejectedException {
user = ctx.getIdentifiedUser();
comments = commentsUtil.draftByChangeAuthor(ctx.getNotes(), ctx.getUser().getAccountId());
// PublishCommentsOp should update a separate ChangeUpdate Object than the one used by other ops
// For example, with the "publish comments on PS upload" workflow,
// There are 2 ops: ReplaceOp & PublishCommentsOp, where each updates its own ChangeUpdate
// This is required since
// 1. a ChangeUpdate has only 1 change message
// 2. Each ChangeUpdate results in 1 commit in NoteDb
// We do it this way so that the execution results in 2 different commits in NoteDb
ChangeUpdate changeUpdate = ctx.getDistinctUpdate(psId);
publishCommentUtil.publish(ctx, changeUpdate, comments, null);
return insertMessage(ctx, changeUpdate);
}
@Override
public void postUpdate(Context ctx) {
if (message == null || comments.isEmpty()) {
return;
}
ChangeNotes changeNotes = changeNotesFactory.createChecked(projectNameKey, psId.changeId());
PatchSet ps = psUtil.get(changeNotes, psId);
NotifyResolver.Result notify = ctx.getNotify(changeNotes.getChangeId());
if (notify.shouldNotify()) {
email.create(notify, changeNotes, ps, user, message, comments, null, labelDelta).sendAsync();
}
try {
commentAdded.fire(
changeNotes.getChange(),
ps,
ctx.getAccount(),
message.getMessage(),
null,
null,
ctx.getWhen());
} catch (Exception e) {
logger.atWarning().withCause(e).log("comment-added event invocation failed");
}
}
private boolean insertMessage(ChangeContext ctx, ChangeUpdate changeUpdate) {
StringBuilder buf = new StringBuilder();
if (comments.size() == 1) {
buf.append("\n\n(1 comment)");
} else if (comments.size() > 1) {
buf.append(String.format("\n\n(%d comments)", comments.size()));
}
if (buf.length() == 0) {
return false;
}
message =
ChangeMessagesUtil.newMessage(
psId, user, ctx.getWhen(), "Patch Set " + psId.get() + ":" + buf, null);
cmUtil.addChangeMessage(changeUpdate, message);
return true;
}
}

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.metrics.Histogram1;
import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer1; import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PublishCommentsOp;
import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ReceiveCommitsExecutor; import com.google.gerrit.server.config.ReceiveCommitsExecutor;
@@ -103,6 +104,7 @@ public class AsyncReceiveCommits implements PreReceiveHook {
// Don't expose the binding for ReceiveCommits.Factory. All callers should // Don't expose the binding for ReceiveCommits.Factory. All callers should
// be using AsyncReceiveCommits.Factory instead. // be using AsyncReceiveCommits.Factory instead.
install(new FactoryModuleBuilder().build(ReceiveCommits.Factory.class)); install(new FactoryModuleBuilder().build(ReceiveCommits.Factory.class));
install(new FactoryModuleBuilder().build(PublishCommentsOp.Factory.class));
install(new FactoryModuleBuilder().build(BranchCommitValidator.Factory.class)); install(new FactoryModuleBuilder().build(BranchCommitValidator.Factory.class));
} }

View File

@@ -102,6 +102,7 @@ import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil; import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.PublishCommentsOp;
import com.google.gerrit.server.RequestInfo; import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener; import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountResolver;
@@ -331,6 +332,7 @@ class ReceiveCommits {
private final RefOperationValidators.Factory refValidatorsFactory; private final RefOperationValidators.Factory refValidatorsFactory;
private final ReplaceOp.Factory replaceOpFactory; private final ReplaceOp.Factory replaceOpFactory;
private final PluginSetContext<RequestListener> requestListeners; private final PluginSetContext<RequestListener> requestListeners;
private final PublishCommentsOp.Factory publishCommentsOp;
private final RetryHelper retryHelper; private final RetryHelper retryHelper;
private final RequestScopePropagator requestScopePropagator; private final RequestScopePropagator requestScopePropagator;
private final Sequences seq; private final Sequences seq;
@@ -403,6 +405,7 @@ class ReceiveCommits {
Provider<InternalChangeQuery> queryProvider, Provider<InternalChangeQuery> queryProvider,
Provider<MergeOp> mergeOpProvider, Provider<MergeOp> mergeOpProvider,
Provider<MergeOpRepoManager> ormProvider, Provider<MergeOpRepoManager> ormProvider,
PublishCommentsOp.Factory publishCommentsOp,
ReceiveConfig receiveConfig, ReceiveConfig receiveConfig,
RefOperationValidators.Factory refValidatorsFactory, RefOperationValidators.Factory refValidatorsFactory,
ReplaceOp.Factory replaceOpFactory, ReplaceOp.Factory replaceOpFactory,
@@ -449,6 +452,7 @@ class ReceiveCommits {
this.projectCache = projectCache; this.projectCache = projectCache;
this.psUtil = psUtil; this.psUtil = psUtil;
this.performanceLoggers = performanceLoggers; this.performanceLoggers = performanceLoggers;
this.publishCommentsOp = publishCommentsOp;
this.queryProvider = queryProvider; this.queryProvider = queryProvider;
this.receiveConfig = receiveConfig; this.receiveConfig = receiveConfig;
this.refValidatorsFactory = refValidatorsFactory; this.refValidatorsFactory = refValidatorsFactory;
@@ -905,10 +909,15 @@ class ReceiveCommits {
logger.atFine().log("Adding %d replace requests", newChanges.size()); logger.atFine().log("Adding %d replace requests", newChanges.size());
for (ReplaceRequest replace : replaceByChange.values()) { for (ReplaceRequest replace : replaceByChange.values()) {
replace.addOps(bu, replaceProgress);
if (magicBranch != null) { if (magicBranch != null) {
bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes)); bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
if (magicBranch.shouldPublishComments()) {
bu.addOp(
replace.notes.getChangeId(),
publishCommentsOp.create(replace.psId, project.getNameKey()));
}
} }
replace.addOps(bu, replaceProgress);
} }
logger.atFine().log("Adding %d create requests", newChanges.size()); logger.atFine().log("Adding %d create requests", newChanges.size());

View File

@@ -32,7 +32,6 @@ import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage; import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval; import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.PatchSetInfo; import com.google.gerrit.entities.PatchSetInfo;
@@ -45,13 +44,10 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.AddReviewersOp; import com.google.gerrit.server.change.AddReviewersOp;
import com.google.gerrit.server.change.ChangeKindCache; import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerAdder; import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput; import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput;
@@ -121,9 +117,6 @@ public class ReplaceOp implements BatchUpdateOp {
private final ChangeData.Factory changeDataFactory; private final ChangeData.Factory changeDataFactory;
private final ChangeKindCache changeKindCache; private final ChangeKindCache changeKindCache;
private final ChangeMessagesUtil cmUtil; private final ChangeMessagesUtil cmUtil;
private final CommentsUtil commentsUtil;
private final PublishCommentUtil publishCommentUtil;
private final EmailReviewComments.Factory emailCommentsFactory;
private final ExecutorService sendEmailExecutor; private final ExecutorService sendEmailExecutor;
private final RevisionCreated revisionCreated; private final RevisionCreated revisionCreated;
private final CommentAdded commentAdded; private final CommentAdded commentAdded;
@@ -154,7 +147,6 @@ public class ReplaceOp implements BatchUpdateOp {
private PatchSet newPatchSet; private PatchSet newPatchSet;
private ChangeKind changeKind; private ChangeKind changeKind;
private ChangeMessage msg; private ChangeMessage msg;
private List<Comment> comments = ImmutableList.of();
private String rejectMessage; private String rejectMessage;
private MergedByPushOp mergedByPushOp; private MergedByPushOp mergedByPushOp;
private RequestScopePropagator requestScopePropagator; private RequestScopePropagator requestScopePropagator;
@@ -168,9 +160,6 @@ public class ReplaceOp implements BatchUpdateOp {
ChangeData.Factory changeDataFactory, ChangeData.Factory changeDataFactory,
ChangeKindCache changeKindCache, ChangeKindCache changeKindCache,
ChangeMessagesUtil cmUtil, ChangeMessagesUtil cmUtil,
CommentsUtil commentsUtil,
PublishCommentUtil publishCommentUtil,
EmailReviewComments.Factory emailCommentsFactory,
RevisionCreated revisionCreated, RevisionCreated revisionCreated,
CommentAdded commentAdded, CommentAdded commentAdded,
MergedByPushOp.Factory mergedByPushOpFactory, MergedByPushOp.Factory mergedByPushOpFactory,
@@ -197,9 +186,6 @@ public class ReplaceOp implements BatchUpdateOp {
this.changeDataFactory = changeDataFactory; this.changeDataFactory = changeDataFactory;
this.changeKindCache = changeKindCache; this.changeKindCache = changeKindCache;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.commentsUtil = commentsUtil;
this.publishCommentUtil = publishCommentUtil;
this.emailCommentsFactory = emailCommentsFactory;
this.revisionCreated = revisionCreated; this.revisionCreated = revisionCreated;
this.commentAdded = commentAdded; this.commentAdded = commentAdded;
this.mergedByPushOpFactory = mergedByPushOpFactory; this.mergedByPushOpFactory = mergedByPushOpFactory;
@@ -302,13 +288,6 @@ public class ReplaceOp implements BatchUpdateOp {
change.setWorkInProgress(true); change.setWorkInProgress(true);
update.setWorkInProgress(true); update.setWorkInProgress(true);
} }
if (shouldPublishComments()) {
boolean workInProgress = change.isWorkInProgress();
if (magicBranch.workInProgress) {
workInProgress = true;
}
comments = publishComments(ctx, workInProgress);
}
} }
newPatchSet = newPatchSet =
@@ -416,11 +395,6 @@ public class ReplaceOp implements BatchUpdateOp {
} else { } else {
message.append('.'); message.append('.');
} }
if (comments.size() == 1) {
message.append("\n\n(1 comment)");
} else if (comments.size() > 1) {
message.append(String.format("\n\n(%d comments)", comments.size()));
}
if (!Strings.isNullOrEmpty(reviewMessage)) { if (!Strings.isNullOrEmpty(reviewMessage)) {
message.append("\n\n").append(reviewMessage); message.append("\n\n").append(reviewMessage);
} }
@@ -499,14 +473,6 @@ public class ReplaceOp implements BatchUpdateOp {
change.setKey(Change.key(idList.get(idList.size() - 1).trim())); change.setKey(Change.key(idList.get(idList.size() - 1).trim()));
} }
private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress) {
List<Comment> comments =
commentsUtil.draftByChangeAuthor(ctx.getNotes(), ctx.getUser().getAccountId());
publishCommentUtil.publish(
ctx, patchSetId, comments, ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
return comments;
}
@Override @Override
public void postUpdate(Context ctx) throws Exception { public void postUpdate(Context ctx) throws Exception {
reviewerAdditions.postUpdate(ctx); reviewerAdditions.postUpdate(ctx);
@@ -520,25 +486,10 @@ public class ReplaceOp implements BatchUpdateOp {
e.run(); e.run();
} }
} }
NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId()); NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
if (shouldPublishComments()) {
emailCommentsFactory
.create(
notify,
notes,
newPatchSet,
ctx.getUser().asIdentifiedUser(),
msg,
comments,
msg.getMessage(),
ImmutableList.of()) // TODO(dborowitz): Include labels.
.sendAsync();
}
revisionCreated.fire(notes.getChange(), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify); revisionCreated.fire(notes.getChange(), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
try { try {
fireCommentAddedEvent(ctx); fireApprovalsEvent(ctx);
} catch (Exception e) { } catch (Exception e) {
logger.atWarning().withCause(e).log("comment-added event invocation failed"); logger.atWarning().withCause(e).log("comment-added event invocation failed");
} }
@@ -588,11 +539,10 @@ public class ReplaceOp implements BatchUpdateOp {
} }
} }
private void fireCommentAddedEvent(Context ctx) throws IOException { private void fireApprovalsEvent(Context ctx) throws IOException {
if (approvals.isEmpty()) { if (approvals.isEmpty()) {
return; return;
} }
/* For labels that are not set in this operation, show the "current" value /* 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. * 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 * For labels that are set in this operation, the value was modified, so
@@ -612,7 +562,6 @@ public class ReplaceOp implements BatchUpdateOp {
oldApprovals.put(entry.getKey(), (short) 0); oldApprovals.put(entry.getKey(), (short) 0);
} }
} }
commentAdded.fire( commentAdded.fire(
notes.getChange(), notes.getChange(),
newPatchSet, newPatchSet,
@@ -663,8 +612,4 @@ public class ReplaceOp implements BatchUpdateOp {
return null; return null;
} }
} }
private boolean shouldPublishComments() {
return magicBranch != null && magicBranch.shouldPublishComments();
}
} }

View File

@@ -1003,7 +1003,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
case PUBLISH: case PUBLISH:
case PUBLISH_ALL_REVISIONS: case PUBLISH_ALL_REVISIONS:
validateComments(Streams.concat(drafts.values().stream(), toPublish.stream())); validateComments(Streams.concat(drafts.values().stream(), toPublish.stream()));
publishCommentUtil.publish(ctx, psId, drafts.values(), in.tag); publishCommentUtil.publish(ctx, ctx.getUpdate(psId), drafts.values(), in.tag);
comments.addAll(drafts.values()); comments.addAll(drafts.values());
break; break;
case KEEP: case KEEP:

View File

@@ -23,6 +23,7 @@ import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
@@ -34,6 +35,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSet.Id;
import com.google.gerrit.entities.Project; import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.config.FactoryModule;
@@ -256,29 +258,54 @@ public class BatchUpdate implements AutoCloseable {
private class ChangeContextImpl extends ContextImpl implements ChangeContext { private class ChangeContextImpl extends ContextImpl implements ChangeContext {
private final ChangeNotes notes; private final ChangeNotes notes;
private final Map<PatchSet.Id, ChangeUpdate> updates;
/**
* Updates where the caller instructed us to create one NoteDb commit per update. Keyed by
* PatchSet.Id only for convenience.
*/
private final Map<PatchSet.Id, ChangeUpdate> defaultUpdates;
/**
* Updates where the caller allowed us to combine potentially multiple adjustments into a single
* commit in NoteDb by re-using the same ChangeUpdate instance. Will still be one commit per
* patch set.
*/
private final ListMultimap<Id, ChangeUpdate> distinctUpdates;
private boolean deleted; private boolean deleted;
ChangeContextImpl(ChangeNotes notes) { ChangeContextImpl(ChangeNotes notes) {
this.notes = requireNonNull(notes); this.notes = requireNonNull(notes);
updates = new TreeMap<>(comparing(PatchSet.Id::get)); defaultUpdates = new TreeMap<>(comparing(PatchSet.Id::get));
distinctUpdates = ArrayListMultimap.create();
} }
@Override @Override
public ChangeUpdate getUpdate(PatchSet.Id psId) { public ChangeUpdate getUpdate(PatchSet.Id psId) {
ChangeUpdate u = updates.get(psId); ChangeUpdate u = defaultUpdates.get(psId);
if (u == null) { if (u == null) {
u = changeUpdateFactory.create(notes, user, when); u = getNewChangeUpdate(psId);
if (newChanges.containsKey(notes.getChangeId())) { defaultUpdates.put(psId, u);
u.setAllowWriteToNewRef(true);
}
u.setPatchSetId(psId);
updates.put(psId, u);
} }
return u; return u;
} }
@Override
public ChangeUpdate getDistinctUpdate(PatchSet.Id psId) {
ChangeUpdate u = getNewChangeUpdate(psId);
distinctUpdates.put(psId, u);
return u;
}
private ChangeUpdate getNewChangeUpdate(PatchSet.Id psId) {
ChangeUpdate u = changeUpdateFactory.create(notes, user, when);
if (newChanges.containsKey(notes.getChangeId())) {
u.setAllowWriteToNewRef(true);
}
u.setPatchSetId(psId);
return u;
}
@Override @Override
public ChangeNotes getNotes() { public ChangeNotes getNotes() {
return notes; return notes;
@@ -571,7 +598,8 @@ public class BatchUpdate implements AutoCloseable {
handle.setResult(id, ChangeResult.SKIPPED); handle.setResult(id, ChangeResult.SKIPPED);
continue; continue;
} }
ctx.updates.values().forEach(handle.manager::add); ctx.defaultUpdates.values().forEach(handle.manager::add);
ctx.distinctUpdates.values().forEach(handle.manager::add);
if (ctx.deleted) { if (ctx.deleted) {
logDebug("Change %s was deleted", id); logDebug("Change %s was deleted", id);
handle.manager.deleteChange(id); handle.manager.deleteChange(id);

View File

@@ -30,7 +30,7 @@ import com.google.gerrit.server.notedb.ChangeUpdate;
*/ */
public interface ChangeContext extends Context { public interface ChangeContext extends Context {
/** /**
* Get an update for this change at a given patch set. * Get the first update for this change at a given patch set.
* *
* <p>A single operation can modify changes at different patch sets. Commits in the NoteDb graph * <p>A single operation can modify changes at different patch sets. Commits in the NoteDb graph
* within this update are created in patch set order. * within this update are created in patch set order.
@@ -42,6 +42,16 @@ public interface ChangeContext extends Context {
*/ */
ChangeUpdate getUpdate(PatchSet.Id psId); ChangeUpdate getUpdate(PatchSet.Id psId);
/**
* Gets a new ChangeUpdate for this change at a given patch set.
*
* <p>To get the current patch set ID, use {@link com.google.gerrit.server.PatchSetUtil#current}.
*
* @param psId patch set ID.
* @return handle for change updates.
*/
ChangeUpdate getDistinctUpdate(PatchSet.Id psId);
/** /**
* Get the up-to-date notes for this change. * Get the up-to-date notes for this change.
* *

View File

@@ -115,7 +115,6 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
@@ -2043,36 +2042,57 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
assertThat(comments.stream().map(c -> c.id)).containsExactly(c1.id, c2.id, c3.id); assertThat(comments.stream().map(c -> c.id)).containsExactly(c1.id, c2.id, c3.id);
assertThat(comments.stream().map(c -> c.message)) assertThat(comments.stream().map(c -> c.message))
.containsExactly("comment1", "comment2", "comment3"); .containsExactly("comment1", "comment2", "comment3");
assertThat(getLastMessage(r.getChangeId())).isEqualTo("Uploaded patch set 3.\n\n(3 comments)");
List<String> messages = /* Assert the correctness of the API messages */
List<ChangeMessageInfo> allMessages = getMessages(r.getChangeId());
List<String> messagesText = allMessages.stream().map(m -> m.message).collect(toList());
assertThat(messagesText)
.containsExactly(
"Uploaded patch set 1.",
"Uploaded patch set 2.",
"Uploaded patch set 3.",
"Patch Set 3:\n\n(3 comments)")
.inOrder();
/* Assert the tags - PS#2 comments do not have tags, PS#3 upload is autogenerated */
List<String> messagesTags = allMessages.stream().map(m -> m.tag).collect(toList());
;
assertThat(messagesTags.get(2)).isEqualTo("autogenerated:gerrit:newPatchSet");
assertThat(messagesTags.get(3)).isNull();
/* Assert the correctness of the emails sent */
List<String> emailMessages =
sender.getMessages().stream() sender.getMessages().stream()
.map(Message::body) .map(Message::body)
.sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1)) .sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1))
.collect(toList()); .collect(toList());
assertThat(messages).hasSize(2); assertThat(emailMessages).hasSize(2);
assertThat(messages.get(0)).contains("Gerrit-MessageType: newpatchset"); assertThat(emailMessages.get(0)).contains("Gerrit-MessageType: newpatchset");
assertThat(messages.get(0)).contains("I'd like you to reexamine a change"); assertThat(emailMessages.get(0)).contains("I'd like you to reexamine a change");
assertThat(messages.get(0)).doesNotContain("Uploaded patch set 3"); assertThat(emailMessages.get(0)).doesNotContain("Uploaded patch set 3");
assertThat(messages.get(1)).contains("Gerrit-MessageType: comment"); assertThat(emailMessages.get(1)).contains("Gerrit-MessageType: comment");
assertThat(messages.get(1)) assertThat(emailMessages.get(1)).contains("Patch Set 3:\n\n(3 comments)");
.containsMatch( assertThat(emailMessages.get(1)).contains("PS1, Line 1:");
Pattern.compile( assertThat(emailMessages.get(1)).contains("PS2, Line 1:");
// A little weird that the comment email contains this text, but it's actually
// what's in the ChangeMessage. Really we should fuse the emails into one, but until /* Assert the correctness of the NoteDb change meta commits */
// then, this test documents the current behavior. List<RevCommit> commitMessages = getChangeMetaCommitsInReverseOrder(r.getChange().getId());
"Uploaded patch set 3\\.\n" assertThat(commitMessages).hasSize(5);
+ "\n" assertThat(commitMessages.get(0).getShortMessage()).isEqualTo("Create change");
+ "\\(3 comments\\)\\n.*" assertThat(commitMessages.get(1).getShortMessage()).isEqualTo("Create patch set 2");
+ "PS1, Line 1:.*" assertThat(commitMessages.get(2).getShortMessage()).isEqualTo("Update patch set 2");
+ "comment1\\n.*" assertThat(commitMessages.get(3).getShortMessage()).isEqualTo("Create patch set 3");
+ "PS1, Line 1:.*" assertThat(commitMessages.get(4).getFullMessage())
+ "comment2\\n.*" .isEqualTo(
+ "PS2, Line 1:.*" "Update patch set 3\n"
+ "comment3\\n", + "\n"
Pattern.DOTALL)); + "Patch Set 3:\n"
+ "\n"
+ "(3 comments)\n"
+ "\n"
+ "Patch-set: 3\n");
} }
@Test @Test
@@ -2085,8 +2105,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
Collection<CommentInfo> comments = getPublishedComments(r.getChangeId()); Collection<CommentInfo> comments = getPublishedComments(r.getChangeId());
assertThat(comments.stream().map(c -> c.message)).containsExactly("comment1"); assertThat(comments.stream().map(c -> c.message)).containsExactly("comment1");
assertThat(getLastMessage(r.getChangeId())) assertThat(getLastMessage(r.getChangeId())).isEqualTo("Patch Set 2:\n" + "\n" + "(1 comment)");
.isEqualTo("Uploaded patch set 2.\n\n(1 comment)\n\nThe message");
} }
@Test @Test
@@ -2104,16 +2123,22 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
amendChanges(initialHead, commits, "refs/for/master%publish-comments"); amendChanges(initialHead, commits, "refs/for/master%publish-comments");
Collection<CommentInfo> cs1 = getPublishedComments(id1); Collection<CommentInfo> cs1 = getPublishedComments(id1);
List<ChangeMessageInfo> messages1 = getMessages(id1);
assertThat(cs1.stream().map(c -> c.message)).containsExactly("comment1"); assertThat(cs1.stream().map(c -> c.message)).containsExactly("comment1");
assertThat(cs1.stream().map(c -> c.id)).containsExactly(c1.id); assertThat(cs1.stream().map(c -> c.id)).containsExactly(c1.id);
assertThat(getLastMessage(id1)) assertThat(messages1.get(0).message).isEqualTo("Uploaded patch set 1.");
.isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)"); assertThat(messages1.get(1).message)
.isEqualTo("Uploaded patch set 2: Commit message was updated.");
assertThat(messages1.get(2).message).isEqualTo("Patch Set 2:\n\n(1 comment)");
Collection<CommentInfo> cs2 = getPublishedComments(id2); Collection<CommentInfo> cs2 = getPublishedComments(id2);
List<ChangeMessageInfo> messages2 = getMessages(id2);
assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2"); assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2");
assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id); assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id);
assertThat(getLastMessage(id2)) assertThat(messages2.get(0).message).isEqualTo("Uploaded patch set 1.");
.isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)"); assertThat(messages2.get(1).message)
.isEqualTo("Uploaded patch set 2: Commit message was updated.");
assertThat(messages2.get(2).message).isEqualTo("Patch Set 2:\n\n(1 comment)");
} }
@Test @Test
@@ -2138,7 +2163,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id); assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id);
assertThat(getLastMessage(id1)).doesNotMatch("[Cc]omment"); assertThat(getLastMessage(id1)).doesNotMatch("[Cc]omment");
assertThat(getLastMessage(id2)).isEqualTo("Uploaded patch set 2.\n\n(1 comment)"); assertThat(getLastMessage(id2)).isEqualTo("Patch Set 2:\n\n(1 comment)");
} }
@Test @Test
@@ -2625,6 +2650,10 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
.get(); .get();
} }
private List<ChangeMessageInfo> getMessages(String changeId) throws Exception {
return gApi.changes().id(changeId).get(MESSAGES).messages.stream().collect(toList());
}
private void assertThatUserIsOnlyReviewer(ChangeInfo ci, TestAccount reviewer) { private void assertThatUserIsOnlyReviewer(ChangeInfo ci, TestAccount reviewer) {
assertThat(ci.reviewers).isNotNull(); assertThat(ci.reviewers).isNotNull();
assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER); assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER);

View File

@@ -125,8 +125,10 @@ public class ReceiveCommitsCommentValidationIT extends AbstractDaemonTest {
assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty(); assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo); amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(2); assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(2);
assertThat(capture.getAllValues()).hasSize(1); assertThat(capture.getAllValues()).hasSize(1);
assertThat(capture.getValue())
assertThat(capture.getAllValues().get(0))
.containsExactly( .containsExactly(
CommentForValidation.create( CommentForValidation.create(
CommentForValidation.CommentType.INLINE_COMMENT, draftInline.message), CommentForValidation.CommentType.INLINE_COMMENT, draftInline.message),