| // 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.acceptance.testsuite.change; | |
| import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.Comment.Status; | |
| import com.google.gerrit.entities.HumanComment; | |
| import com.google.gerrit.entities.Patch; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RobotComment; | |
| import com.google.gerrit.extensions.client.Comment; | |
| import com.google.gerrit.extensions.client.Comment.Range; | |
| import com.google.gerrit.extensions.restapi.RestApiException; | |
| import com.google.gerrit.server.CommentsUtil; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.IdentifiedUser.GenericFactory; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.notedb.ChangeNotes; | |
| import com.google.gerrit.server.notedb.ChangeUpdate; | |
| import com.google.gerrit.server.update.BatchUpdate; | |
| import com.google.gerrit.server.update.BatchUpdateOp; | |
| import com.google.gerrit.server.update.ChangeContext; | |
| import com.google.gerrit.server.update.UpdateException; | |
| import com.google.gerrit.server.update.context.RefUpdateContext; | |
| import com.google.gerrit.server.util.time.TimeUtil; | |
| import com.google.inject.Inject; | |
| import com.google.inject.assistedinject.Assisted; | |
| import java.io.IOException; | |
| import java.time.Instant; | |
| import org.eclipse.jgit.lib.ObjectInserter; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| /** | |
| * The implementation of {@link PerPatchsetOperations}. | |
| * | |
| * <p>There is only one implementation of {@link PerPatchsetOperations}. Nevertheless, we keep the | |
| * separation between interface and implementation to enhance clarity. | |
| */ | |
| public class PerPatchsetOperationsImpl implements PerPatchsetOperations { | |
| private final GitRepositoryManager repositoryManager; | |
| private final IdentifiedUser.GenericFactory userFactory; | |
| private final BatchUpdate.Factory batchUpdateFactory; | |
| private final CommentsUtil commentsUtil; | |
| private final ChangeNotes changeNotes; | |
| private final PatchSet.Id patchsetId; | |
| public interface Factory { | |
| PerPatchsetOperationsImpl create(ChangeNotes changeNotes, PatchSet.Id patchsetId); | |
| } | |
| @Inject | |
| private PerPatchsetOperationsImpl( | |
| GitRepositoryManager repositoryManager, | |
| GenericFactory userFactory, | |
| BatchUpdate.Factory batchUpdateFactory, | |
| CommentsUtil commentsUtil, | |
| @Assisted ChangeNotes changeNotes, | |
| @Assisted PatchSet.Id patchsetId) { | |
| this.repositoryManager = repositoryManager; | |
| this.userFactory = userFactory; | |
| this.batchUpdateFactory = batchUpdateFactory; | |
| this.commentsUtil = commentsUtil; | |
| this.changeNotes = changeNotes; | |
| this.patchsetId = patchsetId; | |
| } | |
| @Override | |
| public TestPatchset get() { | |
| PatchSet patchset = changeNotes.getPatchSets().get(patchsetId); | |
| return TestPatchset.builder().patchsetId(patchsetId).commitId(patchset.commitId()).build(); | |
| } | |
| @Override | |
| public TestCommentCreation.Builder newComment() { | |
| return TestCommentCreation.builder(this::createComment, Status.PUBLISHED); | |
| } | |
| @Override | |
| public TestCommentCreation.Builder newDraftComment() { | |
| return TestCommentCreation.builder(this::createComment, Status.DRAFT); | |
| } | |
| @Override | |
| public TestRobotCommentCreation.Builder newRobotComment() { | |
| return TestRobotCommentCreation.builder(this::createRobotComment); | |
| } | |
| private String createComment(TestCommentCreation commentCreation) | |
| throws IOException, RestApiException, UpdateException { | |
| Project.NameKey project = changeNotes.getProjectName(); | |
| try (RefUpdateContext ctx = openTestRefUpdateContext()) { | |
| try (Repository repository = repositoryManager.openRepository(project); | |
| ObjectInserter objectInserter = repository.newObjectInserter(); | |
| RevWalk revWalk = new RevWalk(objectInserter.newReader())) { | |
| Instant now = TimeUtil.now(); | |
| IdentifiedUser author = getAuthor(commentCreation); | |
| CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation); | |
| try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) { | |
| batchUpdate.setRepository(repository, revWalk, objectInserter); | |
| batchUpdate.addOp(changeNotes.getChangeId(), commentAdditionOp); | |
| batchUpdate.execute(); | |
| } | |
| return commentAdditionOp.createdCommentUuid; | |
| } | |
| } | |
| } | |
| private IdentifiedUser getAuthor(TestCommentCreation commentCreation) { | |
| Account.Id authorId = commentCreation.author().orElse(changeNotes.getChange().getOwner()); | |
| return userFactory.create(authorId); | |
| } | |
| private IdentifiedUser getAuthor(TestRobotCommentCreation robotCommentCreation) { | |
| Account.Id authorId = robotCommentCreation.author().orElse(changeNotes.getChange().getOwner()); | |
| return userFactory.create(authorId); | |
| } | |
| private static Comment.Range toCommentRange(TestRange range) { | |
| Comment.Range commentRange = new Range(); | |
| commentRange.startLine = range.start().line(); | |
| commentRange.startCharacter = range.start().charOffset(); | |
| commentRange.endLine = range.end().line(); | |
| commentRange.endCharacter = range.end().charOffset(); | |
| return commentRange; | |
| } | |
| private class CommentAdditionOp implements BatchUpdateOp { | |
| private String createdCommentUuid; | |
| private final TestCommentCreation commentCreation; | |
| public CommentAdditionOp(TestCommentCreation commentCreation) { | |
| this.commentCreation = commentCreation; | |
| } | |
| @Override | |
| public boolean updateChange(ChangeContext context) { | |
| HumanComment comment = toNewComment(context, commentCreation); | |
| ChangeUpdate changeUpdate = context.getUpdate(patchsetId); | |
| changeUpdate.putComment(commentCreation.status(), comment); | |
| // For published comments, only the tag set on the ChangeUpdate (and not on the HumanComment) | |
| // matters. | |
| commentCreation.tag().ifPresent(changeUpdate::setTag); | |
| createdCommentUuid = comment.key.uuid; | |
| return true; | |
| } | |
| private HumanComment toNewComment(ChangeContext context, TestCommentCreation commentCreation) { | |
| String message = commentCreation.message().orElse("The text of a test comment."); | |
| String filePath = commentCreation.file().orElse(Patch.PATCHSET_LEVEL); | |
| short side = commentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide(); | |
| Boolean unresolved = commentCreation.unresolved().orElse(null); | |
| String parentUuid = commentCreation.parentUuid().orElse(null); | |
| Instant createdOn = commentCreation.createdOn().orElse(context.getWhen()); | |
| HumanComment newComment = | |
| commentsUtil.newHumanComment( | |
| context.getNotes(), | |
| context.getUser(), | |
| createdOn, | |
| filePath, | |
| patchsetId, | |
| side, | |
| message, | |
| unresolved, | |
| parentUuid); | |
| // For draft comments, only the tag set on the HumanComment (and not on the ChangeUpdate) | |
| // matters. | |
| commentCreation.tag().ifPresent(tag -> newComment.tag = tag); | |
| commentCreation.line().ifPresent(line -> newComment.setLineNbrAndRange(line, null)); | |
| // Specification of range trumps explicit line specification. | |
| commentCreation | |
| .range() | |
| .map(PerPatchsetOperationsImpl::toCommentRange) | |
| .ifPresent(range -> newComment.setLineNbrAndRange(null, range)); | |
| commentsUtil.setCommentCommitId( | |
| newComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId)); | |
| return newComment; | |
| } | |
| } | |
| private String createRobotComment(TestRobotCommentCreation robotCommentCreation) | |
| throws IOException, RestApiException, UpdateException { | |
| Project.NameKey project = changeNotes.getProjectName(); | |
| try (RefUpdateContext ctx = openTestRefUpdateContext()) { | |
| try (Repository repository = repositoryManager.openRepository(project); | |
| ObjectInserter objectInserter = repository.newObjectInserter(); | |
| RevWalk revWalk = new RevWalk(objectInserter.newReader())) { | |
| Instant now = TimeUtil.now(); | |
| IdentifiedUser author = getAuthor(robotCommentCreation); | |
| RobotCommentAdditionOp robotCommentAdditionOp = | |
| new RobotCommentAdditionOp(robotCommentCreation); | |
| try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) { | |
| batchUpdate.setRepository(repository, revWalk, objectInserter); | |
| batchUpdate.addOp(changeNotes.getChangeId(), robotCommentAdditionOp); | |
| batchUpdate.execute(); | |
| } | |
| return robotCommentAdditionOp.createdRobotCommentUuid; | |
| } | |
| } | |
| } | |
| private class RobotCommentAdditionOp implements BatchUpdateOp { | |
| private String createdRobotCommentUuid; | |
| private final TestRobotCommentCreation robotCommentCreation; | |
| public RobotCommentAdditionOp(TestRobotCommentCreation robotCommentCreation) { | |
| this.robotCommentCreation = robotCommentCreation; | |
| } | |
| @Override | |
| public boolean updateChange(ChangeContext context) { | |
| RobotComment robotComment = toNewRobotComment(context, robotCommentCreation); | |
| ChangeUpdate changeUpdate = context.getUpdate(patchsetId); | |
| changeUpdate.putRobotComment(robotComment); | |
| // For robot comments, only the tag set on the ChangeUpdate (and not on the RobotComment) | |
| // matters. | |
| robotCommentCreation.tag().ifPresent(changeUpdate::setTag); | |
| createdRobotCommentUuid = robotComment.key.uuid; | |
| return true; | |
| } | |
| private RobotComment toNewRobotComment( | |
| ChangeContext context, TestRobotCommentCreation robotCommentCreation) { | |
| String message = robotCommentCreation.message().orElse("The text of a test robot comment."); | |
| String filePath = robotCommentCreation.file().orElse(Patch.PATCHSET_LEVEL); | |
| short side = robotCommentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide(); | |
| String robotId = robotCommentCreation.robotId().orElse("robot"); | |
| String robotRunId = robotCommentCreation.robotId().orElse("1"); | |
| RobotComment newRobotComment = | |
| commentsUtil.newRobotComment( | |
| context, filePath, patchsetId, side, message, robotId, robotRunId); | |
| // TODO(paiking): This should not be needed, as the tag only matters in ChangeUpdate. | |
| robotCommentCreation.tag().ifPresent(tag -> newRobotComment.tag = tag); | |
| robotCommentCreation.line().ifPresent(line -> newRobotComment.setLineNbrAndRange(line, null)); | |
| // Specification of range trumps explicit line specification. | |
| robotCommentCreation | |
| .range() | |
| .map(PerPatchsetOperationsImpl::toCommentRange) | |
| .ifPresent(range -> newRobotComment.setLineNbrAndRange(null, range)); | |
| robotCommentCreation | |
| .parentUuid() | |
| .ifPresent(parentUuid -> newRobotComment.parentUuid = parentUuid); | |
| robotCommentCreation.url().ifPresent(url -> newRobotComment.url = url); | |
| if (!robotCommentCreation.properties().isEmpty()) { | |
| newRobotComment.properties = robotCommentCreation.properties(); | |
| } | |
| commentsUtil.setCommentCommitId( | |
| newRobotComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId)); | |
| return newRobotComment; | |
| } | |
| } | |
| } |