blob: 9c8b369ee878c28f033bb013a7bba8af1dfdd01f [file] [log] [blame]
// Copyright (C) 2017 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.notedb;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.entities.Comment.Status;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.RefNames;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* Deletes a published comment from NoteDb by rewriting the commit history. Instead of deleting the
* whole comment, it just replaces the comment's message with a new message.
*/
public class DeleteCommentRewriter implements NoteDbRewriter {
public interface Factory {
/**
* Creates a DeleteCommentRewriter instance.
*
* @param id the id of the change which contains the target comment.
* @param uuid the uuid of the target comment.
* @param newMessage the message used to replace the old message of the target comment.
* @return the DeleteCommentRewriter instance
*/
DeleteCommentRewriter create(
Change.Id id, @Assisted("uuid") String uuid, @Assisted("newMessage") String newMessage);
}
private final ChangeNoteUtil noteUtil;
private final Change.Id changeId;
private final String uuid;
private final String newMessage;
@Inject
DeleteCommentRewriter(
ChangeNoteUtil noteUtil,
@Assisted Change.Id changeId,
@Assisted("uuid") String uuid,
@Assisted("newMessage") String newMessage) {
this.noteUtil = noteUtil;
this.changeId = changeId;
this.uuid = uuid;
this.newMessage = newMessage;
}
@Override
public String getRefName() {
return RefNames.changeMetaRef(changeId);
}
@Override
public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
throws IOException, ConfigInvalidException {
checkArgument(!currTip.equals(ObjectId.zeroId()));
// Walk from the first commit of the branch.
revWalk.reset();
revWalk.markStart(revWalk.parseCommit(currTip));
revWalk.sort(RevSort.REVERSE);
ObjectReader reader = revWalk.getObjectReader();
RevCommit newTipCommit = revWalk.next(); // The first commit will not be rewritten.
Map<String, Comment> parentComments =
getPublishedComments(noteUtil, reader, NoteMap.read(reader, newTipCommit));
boolean rewrite = false;
RevCommit originalCommit;
while ((originalCommit = revWalk.next()) != null) {
NoteMap noteMap = NoteMap.read(reader, originalCommit);
Map<String, Comment> currComments = getPublishedComments(noteUtil, reader, noteMap);
if (!rewrite && currComments.containsKey(uuid)) {
rewrite = true;
}
if (!rewrite) {
parentComments = currComments;
newTipCommit = originalCommit;
continue;
}
List<Comment> putInComments = getPutInComments(parentComments, currComments);
List<Comment> deletedComments = getDeletedComments(parentComments, currComments);
newTipCommit =
revWalk.parseCommit(
rewriteCommit(
originalCommit, newTipCommit, inserter, reader, putInComments, deletedComments));
parentComments = currComments;
}
return newTipCommit;
}
/**
* Gets all the comments which are presented at a commit. Note they include the comments put in by
* the previous commits.
*/
@VisibleForTesting
public static Map<String, Comment> getPublishedComments(
ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
throws IOException, ConfigInvalidException {
return RevisionNoteMap.parse(changeNoteJson, reader, noteMap, Status.PUBLISHED).revisionNotes
.values().stream()
.flatMap(n -> n.getEntities().stream())
.collect(toMap(c -> c.key.uuid, Function.identity()));
}
public static Map<String, Comment> getPublishedComments(
ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap)
throws IOException, ConfigInvalidException {
return getPublishedComments(noteUtil.getChangeNoteJson(), reader, noteMap);
}
/**
* Gets the comments put in by the current commit. The message of the target comment will be
* replaced by the new message.
*
* @param parMap the comment map of the parent commit.
* @param curMap the comment map of the current commit.
* @return The comments put in by the current commit.
*/
private List<Comment> getPutInComments(Map<String, Comment> parMap, Map<String, Comment> curMap) {
List<Comment> comments = new ArrayList<>();
for (String key : curMap.keySet()) {
if (!parMap.containsKey(key)) {
Comment comment = curMap.get(key);
if (key.equals(uuid)) {
comment.message = newMessage;
}
comments.add(comment);
}
}
return comments;
}
/**
* Gets the comments deleted by the current commit.
*
* @param parMap the comment map of the parent commit.
* @param curMap the comment map of the current commit.
* @return The comments deleted by the current commit.
*/
private List<Comment> getDeletedComments(
Map<String, Comment> parMap, Map<String, Comment> curMap) {
return parMap.entrySet().stream()
.filter(c -> !curMap.containsKey(c.getKey()))
.map(Map.Entry::getValue)
.collect(toList());
}
/**
* Rewrites one commit.
*
* @param originalCommit the original commit to be rewritten.
* @param parentCommit the parent of the new commit.
* @param inserter the {@code ObjectInserter} for the rewrite process.
* @param reader the {@code ObjectReader} for the rewrite process.
* @param putInComments the comments put in by this commit.
* @param deletedComments the comments deleted by this commit.
* @return the {@code objectId} of the new commit.
* @throws IOException
* @throws ConfigInvalidException
*/
private ObjectId rewriteCommit(
RevCommit originalCommit,
RevCommit parentCommit,
ObjectInserter inserter,
ObjectReader reader,
List<Comment> putInComments,
List<Comment> deletedComments)
throws IOException, ConfigInvalidException {
RevisionNoteMap<ChangeRevisionNote> revNotesMap =
RevisionNoteMap.parse(
noteUtil.getChangeNoteJson(),
reader,
NoteMap.read(reader, parentCommit),
Status.PUBLISHED);
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNotesMap);
for (Comment c : putInComments) {
cache.get(c.getCommitId()).putComment(c);
}
for (Comment c : deletedComments) {
cache.get(c.getCommitId()).deleteComment(c.key);
}
Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
for (Map.Entry<ObjectId, RevisionNoteBuilder> entry : builders.entrySet()) {
ObjectId objectId = entry.getKey();
byte[] data = entry.getValue().build(noteUtil.getChangeNoteJson());
if (data.length == 0) {
revNotesMap.noteMap.remove(objectId);
} else {
revNotesMap.noteMap.set(objectId, inserter.insert(OBJ_BLOB, data));
}
}
CommitBuilder cb = new CommitBuilder();
cb.setParentId(parentCommit);
cb.setTreeId(revNotesMap.noteMap.writeTree(inserter));
cb.setMessage(originalCommit.getFullMessage());
cb.setCommitter(originalCommit.getCommitterIdent());
cb.setAuthor(originalCommit.getAuthorIdent());
cb.setEncoding(originalCommit.getEncoding());
return inserter.insert(cb);
}
}