| // Copyright (C) 2018 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.common.base.Preconditions.checkState; |
| import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange; |
| import static java.util.Objects.requireNonNull; |
| import static org.eclipse.jgit.util.RawParseUtils.decode; |
| |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.RefNames; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.Optional; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevSort; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.util.RawParseUtils; |
| |
| /** |
| * Deletes a change message from NoteDb by rewriting the commit history. After deletion, the whole |
| * change message will be replaced by a new message indicating the original change message has been |
| * deleted for the given reason. |
| */ |
| public class DeleteChangeMessageRewriter implements NoteDbRewriter { |
| |
| private final Change.Id changeId; |
| private final String targetMessageId; |
| private final String newChangeMessage; |
| |
| DeleteChangeMessageRewriter(Change.Id changeId, String targetMessageId, String newChangeMessage) { |
| this.changeId = changeId; |
| this.targetMessageId = requireNonNull(targetMessageId); |
| this.newChangeMessage = newChangeMessage; |
| } |
| |
| @Override |
| public String getRefName() { |
| return RefNames.changeMetaRef(changeId); |
| } |
| |
| @Override |
| public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip) |
| throws IOException { |
| checkArgument(!currTip.equals(ObjectId.zeroId())); |
| |
| // Walk from the first commit of the branch. |
| revWalk.reset(); |
| revWalk.markStart(revWalk.parseCommit(currTip)); |
| revWalk.sort(RevSort.TOPO); |
| revWalk.sort(RevSort.REVERSE); |
| |
| ObjectId newTipId = null; |
| RevCommit originalCommit; |
| boolean startRewrite = false; |
| while ((originalCommit = revWalk.next()) != null) { |
| boolean isTargetCommit = originalCommit.getId().getName().equals(targetMessageId); |
| if (!startRewrite && !isTargetCommit) { |
| newTipId = originalCommit; |
| continue; |
| } |
| |
| startRewrite = true; |
| String newCommitMessage = |
| isTargetCommit ? createNewCommitMessage(originalCommit) : originalCommit.getFullMessage(); |
| newTipId = rewriteOneCommit(originalCommit, newTipId, newCommitMessage, inserter); |
| } |
| return newTipId; |
| } |
| |
| private String createNewCommitMessage(RevCommit commit) { |
| byte[] raw = commit.getRawBuffer(); |
| |
| Optional<ChangeNoteUtil.CommitMessageRange> range = parseCommitMessageRange(commit); |
| checkState( |
| range.isPresent() && range.get().hasChangeMessage(), "failed to parse commit message"); |
| |
| // Only replace the commit message body, which is the user-provided message. The subject and |
| // footers are NoteDb metadata. |
| Charset encoding = RawParseUtils.parseEncoding(raw); |
| String prefix = |
| decode(encoding, raw, range.get().subjectStart(), range.get().changeMessageStart()); |
| String postfix = decode(encoding, raw, range.get().changeMessageEnd() + 1, raw.length); |
| return prefix + newChangeMessage + postfix; |
| } |
| |
| /** |
| * Rewrites one commit. |
| * |
| * @param originalCommit the original commit to be rewritten. |
| * @param parentCommitId the parent of the new commit. For the first rewritten commit, it's the |
| * parent of 'originalCommit'. For the latter rewritten commits, it's the commit rewritten |
| * just before it. |
| * @param commitMessage the full commit message of the new commit. |
| * @param inserter the {@code ObjectInserter} for the rewrite process. |
| * @return the {@code objectId} of the new commit. |
| */ |
| private ObjectId rewriteOneCommit( |
| RevCommit originalCommit, |
| ObjectId parentCommitId, |
| String commitMessage, |
| ObjectInserter inserter) |
| throws IOException { |
| CommitBuilder cb = new CommitBuilder(); |
| if (parentCommitId != null) { |
| cb.setParentId(parentCommitId); |
| } |
| cb.setTreeId(originalCommit.getTree()); |
| cb.setMessage(commitMessage); |
| cb.setCommitter(originalCommit.getCommitterIdent()); |
| cb.setAuthor(originalCommit.getAuthorIdent()); |
| cb.setEncoding(originalCommit.getEncoding()); |
| return inserter.insert(cb); |
| } |
| } |