Line data Source code
1 : // Copyright (C) 2018 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server; 16 : 17 : import static com.google.common.base.Preconditions.checkArgument; 18 : import static java.util.stream.Collectors.toSet; 19 : 20 : import com.google.common.collect.ImmutableList; 21 : import com.google.common.flogger.FluentLogger; 22 : import com.google.gerrit.common.Nullable; 23 : import com.google.gerrit.entities.HumanComment; 24 : import com.google.gerrit.entities.PatchSet; 25 : import com.google.gerrit.extensions.validators.CommentForValidation; 26 : import com.google.gerrit.extensions.validators.CommentValidationContext; 27 : import com.google.gerrit.extensions.validators.CommentValidationFailure; 28 : import com.google.gerrit.extensions.validators.CommentValidator; 29 : import com.google.gerrit.server.notedb.ChangeNotes; 30 : import com.google.gerrit.server.notedb.ChangeUpdate; 31 : import com.google.gerrit.server.plugincontext.PluginSetContext; 32 : import com.google.gerrit.server.update.ChangeContext; 33 : import com.google.inject.Inject; 34 : import com.google.inject.Singleton; 35 : import java.sql.Timestamp; 36 : import java.util.Collection; 37 : import java.util.HashSet; 38 : import java.util.Map; 39 : import java.util.Set; 40 : 41 : @Singleton 42 : public class PublishCommentUtil { 43 142 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 44 : 45 : private final PatchSetUtil psUtil; 46 : private final CommentsUtil commentsUtil; 47 : 48 : @Inject 49 142 : PublishCommentUtil(CommentsUtil commentsUtil, PatchSetUtil psUtil) { 50 142 : this.commentsUtil = commentsUtil; 51 142 : this.psUtil = psUtil; 52 142 : } 53 : 54 : public void publish( 55 : ChangeContext ctx, 56 : ChangeUpdate changeUpdate, 57 : Collection<HumanComment> draftComments, 58 : @Nullable String tag) { 59 12 : ChangeNotes notes = ctx.getNotes(); 60 12 : checkArgument(notes != null); 61 12 : if (draftComments.isEmpty()) { 62 4 : return; 63 : } 64 : 65 9 : Map<PatchSet.Id, PatchSet> patchSets = 66 9 : psUtil.getAsMap(notes, draftComments.stream().map(d -> psId(notes, d)).collect(toSet())); 67 9 : Set<HumanComment> commentsToPublish = new HashSet<>(); 68 9 : for (HumanComment draftComment : draftComments) { 69 9 : PatchSet.Id psIdOfDraftComment = psId(notes, draftComment); 70 9 : PatchSet ps = patchSets.get(psIdOfDraftComment); 71 9 : if (ps == null) { 72 : // This can happen if changes with the same numeric ID exist: 73 : // - change 12345 has 3 patch sets in repo X 74 : // - another change 12345 has 7 patch sets in repo Y 75 : // - the user saves a draft comment on patch set 6 of the change in repo Y 76 : // - this draft comment gets stored in: 77 : // AllUsers -> refs/draft-comments/45/12345/<account-id> 78 : // - when posting a review with draft handling PUBLISH_ALL_REVISIONS on the change in 79 : // repo X, the draft comments are loaded from 80 : // AllUsers -> refs/draft-comments/45/12345/<account-id>, including the draft 81 : // comment that was saved for patch set 6 of the change in repo Y 82 : // - patch set 6 does not exist for the change in repo x, hence we get null for the patch 83 : // set here 84 : // Instead of failing hard (and returning an Internal Server Error) to the caller, 85 : // just ignore that comment. 86 : // Gerrit ensures that numeric change IDs are unique, but you can get duplicates if 87 : // change refs of one repo are copied/pushed to another repo on the same host (this 88 : // should never be done, but we know it happens). 89 0 : logger.atWarning().log( 90 : "Ignoring draft comment %s on non existing patch set %s (repo = %s)", 91 0 : draftComment, psIdOfDraftComment, notes.getProjectName()); 92 0 : continue; 93 : } 94 9 : draftComment.writtenOn = Timestamp.from(ctx.getWhen()); 95 9 : draftComment.tag = tag; 96 : // Draft may have been created by a different real user; copy the current real user. (Only 97 : // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.) 98 9 : ctx.getUser().updateRealAccountId(draftComment::setRealAuthor); 99 9 : commentsUtil.setCommentCommitId(draftComment, notes.getChange(), ps); 100 9 : commentsToPublish.add(draftComment); 101 9 : } 102 9 : commentsUtil.putHumanComments(changeUpdate, HumanComment.Status.PUBLISHED, commentsToPublish); 103 9 : } 104 : 105 : private static PatchSet.Id psId(ChangeNotes notes, HumanComment c) { 106 9 : return PatchSet.id(notes.getChangeId(), c.key.patchSetId); 107 : } 108 : 109 : /** 110 : * Helper to run the specified set of {@link CommentValidator}-s on the specified comments. 111 : * 112 : * @return See {@link CommentValidator#validateComments(CommentValidationContext,ImmutableList)}. 113 : */ 114 : public static ImmutableList<CommentValidationFailure> findInvalidComments( 115 : CommentValidationContext ctx, 116 : PluginSetContext<CommentValidator> commentValidators, 117 : ImmutableList<CommentForValidation> commentsForValidation) { 118 65 : ImmutableList.Builder<CommentValidationFailure> commentValidationFailures = 119 : new ImmutableList.Builder<>(); 120 65 : commentValidators.runEach( 121 : validator -> 122 65 : commentValidationFailures.addAll( 123 65 : validator.validateComments(ctx, commentsForValidation))); 124 65 : return commentValidationFailures.build(); 125 : } 126 : }