Line data Source code
1 : // Copyright (C) 2017 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.notedb;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static java.util.stream.Collectors.toList;
19 : import static java.util.stream.Collectors.toMap;
20 : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
21 :
22 : import com.google.common.annotations.VisibleForTesting;
23 : import com.google.gerrit.entities.Change;
24 : import com.google.gerrit.entities.HumanComment;
25 : import com.google.gerrit.entities.RefNames;
26 : import com.google.inject.Inject;
27 : import com.google.inject.assistedinject.Assisted;
28 : import java.io.IOException;
29 : import java.util.ArrayList;
30 : import java.util.List;
31 : import java.util.Map;
32 : import java.util.function.Function;
33 : import org.eclipse.jgit.errors.ConfigInvalidException;
34 : import org.eclipse.jgit.lib.CommitBuilder;
35 : import org.eclipse.jgit.lib.ObjectId;
36 : import org.eclipse.jgit.lib.ObjectInserter;
37 : import org.eclipse.jgit.lib.ObjectReader;
38 : import org.eclipse.jgit.notes.NoteMap;
39 : import org.eclipse.jgit.revwalk.RevCommit;
40 : import org.eclipse.jgit.revwalk.RevSort;
41 : import org.eclipse.jgit.revwalk.RevWalk;
42 :
43 : /**
44 : * Deletes a published comment from NoteDb by rewriting the commit history. Instead of deleting the
45 : * whole comment, it just replaces the comment's message with a new message.
46 : */
47 : public class DeleteCommentRewriter implements NoteDbRewriter {
48 :
49 : public interface Factory {
50 : /**
51 : * Creates a DeleteCommentRewriter instance.
52 : *
53 : * @param id the id of the change which contains the target comment.
54 : * @param uuid the uuid of the target comment.
55 : * @param newMessage the message used to replace the old message of the target comment.
56 : * @return the DeleteCommentRewriter instance
57 : */
58 : DeleteCommentRewriter create(
59 : Change.Id id, @Assisted("uuid") String uuid, @Assisted("newMessage") String newMessage);
60 : }
61 :
62 : private final ChangeNoteUtil noteUtil;
63 : private final Change.Id changeId;
64 : private final String uuid;
65 : private final String newMessage;
66 :
67 : @Inject
68 : DeleteCommentRewriter(
69 : ChangeNoteUtil noteUtil,
70 : @Assisted Change.Id changeId,
71 : @Assisted("uuid") String uuid,
72 3 : @Assisted("newMessage") String newMessage) {
73 3 : this.noteUtil = noteUtil;
74 3 : this.changeId = changeId;
75 3 : this.uuid = uuid;
76 3 : this.newMessage = newMessage;
77 3 : }
78 :
79 : @Override
80 : public String getRefName() {
81 3 : return RefNames.changeMetaRef(changeId);
82 : }
83 :
84 : @Override
85 : public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
86 : throws IOException, ConfigInvalidException {
87 3 : checkArgument(!currTip.equals(ObjectId.zeroId()));
88 :
89 : // Walk from the first commit of the branch.
90 3 : revWalk.reset();
91 3 : revWalk.markStart(revWalk.parseCommit(currTip));
92 3 : revWalk.sort(RevSort.REVERSE);
93 :
94 3 : ObjectReader reader = revWalk.getObjectReader();
95 3 : RevCommit newTipCommit = revWalk.next(); // The first commit will not be rewritten.
96 3 : Map<String, HumanComment> parentComments =
97 3 : getPublishedComments(noteUtil, reader, NoteMap.read(reader, newTipCommit));
98 :
99 3 : boolean rewrite = false;
100 : RevCommit originalCommit;
101 3 : while ((originalCommit = revWalk.next()) != null) {
102 3 : NoteMap noteMap = NoteMap.read(reader, originalCommit);
103 3 : Map<String, HumanComment> currComments = getPublishedComments(noteUtil, reader, noteMap);
104 :
105 3 : if (!rewrite && currComments.containsKey(uuid)) {
106 3 : rewrite = true;
107 : }
108 :
109 3 : if (!rewrite) {
110 3 : parentComments = currComments;
111 3 : newTipCommit = originalCommit;
112 3 : continue;
113 : }
114 :
115 3 : List<HumanComment> putInComments = getPutInComments(parentComments, currComments);
116 3 : List<HumanComment> deletedComments = getDeletedComments(parentComments, currComments);
117 3 : newTipCommit =
118 3 : revWalk.parseCommit(
119 3 : rewriteCommit(
120 : originalCommit, newTipCommit, inserter, reader, putInComments, deletedComments));
121 3 : parentComments = currComments;
122 3 : }
123 :
124 3 : return newTipCommit;
125 : }
126 :
127 : /**
128 : * Gets all the comments which are presented at a commit. Note they include the comments put in by
129 : * the previous commits.
130 : */
131 : @VisibleForTesting
132 : public static Map<String, HumanComment> getPublishedComments(
133 : ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
134 : throws IOException, ConfigInvalidException {
135 3 : return RevisionNoteMap.parse(changeNoteJson, reader, noteMap, HumanComment.Status.PUBLISHED)
136 3 : .revisionNotes.values().stream()
137 3 : .flatMap(n -> n.getEntities().stream())
138 3 : .collect(toMap(c -> c.key.uuid, Function.identity()));
139 : }
140 :
141 : public static Map<String, HumanComment> getPublishedComments(
142 : ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap)
143 : throws IOException, ConfigInvalidException {
144 3 : return getPublishedComments(noteUtil.getChangeNoteJson(), reader, noteMap);
145 : }
146 : /**
147 : * Gets the comments put in by the current commit. The message of the target comment will be
148 : * replaced by the new message.
149 : *
150 : * @param parMap the comment map of the parent commit.
151 : * @param curMap the comment map of the current commit.
152 : * @return The comments put in by the current commit.
153 : */
154 : private List<HumanComment> getPutInComments(
155 : Map<String, HumanComment> parMap, Map<String, HumanComment> curMap) {
156 3 : List<HumanComment> comments = new ArrayList<>();
157 3 : for (String key : curMap.keySet()) {
158 3 : if (!parMap.containsKey(key)) {
159 3 : HumanComment comment = curMap.get(key);
160 3 : if (key.equals(uuid)) {
161 3 : comment.message = newMessage;
162 3 : comment.unresolved = false;
163 : }
164 3 : comments.add(comment);
165 : }
166 3 : }
167 3 : return comments;
168 : }
169 :
170 : /**
171 : * Gets the comments deleted by the current commit.
172 : *
173 : * @param parMap the comment map of the parent commit.
174 : * @param curMap the comment map of the current commit.
175 : * @return The comments deleted by the current commit.
176 : */
177 : private List<HumanComment> getDeletedComments(
178 : Map<String, HumanComment> parMap, Map<String, HumanComment> curMap) {
179 3 : return parMap.entrySet().stream()
180 3 : .filter(c -> !curMap.containsKey(c.getKey()))
181 3 : .map(Map.Entry::getValue)
182 3 : .collect(toList());
183 : }
184 :
185 : /**
186 : * Rewrites one commit.
187 : *
188 : * @param originalCommit the original commit to be rewritten.
189 : * @param parentCommit the parent of the new commit.
190 : * @param inserter the {@code ObjectInserter} for the rewrite process.
191 : * @param reader the {@code ObjectReader} for the rewrite process.
192 : * @param putInComments the comments put in by this commit.
193 : * @param deletedComments the comments deleted by this commit.
194 : * @return the {@code objectId} of the new commit.
195 : */
196 : private ObjectId rewriteCommit(
197 : RevCommit originalCommit,
198 : RevCommit parentCommit,
199 : ObjectInserter inserter,
200 : ObjectReader reader,
201 : List<HumanComment> putInComments,
202 : List<HumanComment> deletedComments)
203 : throws IOException, ConfigInvalidException {
204 3 : RevisionNoteMap<ChangeRevisionNote> revNotesMap =
205 3 : RevisionNoteMap.parse(
206 3 : noteUtil.getChangeNoteJson(),
207 : reader,
208 3 : NoteMap.read(reader, parentCommit),
209 : HumanComment.Status.PUBLISHED);
210 3 : RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNotesMap);
211 :
212 3 : for (HumanComment c : putInComments) {
213 3 : cache.get(c.getCommitId()).putComment(c);
214 3 : }
215 :
216 3 : for (HumanComment c : deletedComments) {
217 0 : cache.get(c.getCommitId()).deleteComment(c.key);
218 0 : }
219 :
220 3 : Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
221 3 : for (Map.Entry<ObjectId, RevisionNoteBuilder> entry : builders.entrySet()) {
222 3 : ObjectId objectId = entry.getKey();
223 3 : byte[] data = entry.getValue().build(noteUtil.getChangeNoteJson());
224 3 : if (data.length == 0) {
225 0 : revNotesMap.noteMap.remove(objectId);
226 : } else {
227 3 : revNotesMap.noteMap.set(objectId, inserter.insert(OBJ_BLOB, data));
228 : }
229 3 : }
230 :
231 3 : CommitBuilder cb = new CommitBuilder();
232 3 : cb.setParentId(parentCommit);
233 3 : cb.setTreeId(revNotesMap.noteMap.writeTree(inserter));
234 3 : cb.setMessage(originalCommit.getFullMessage());
235 3 : cb.setCommitter(originalCommit.getCommitterIdent());
236 3 : cb.setAuthor(originalCommit.getAuthorIdent());
237 3 : cb.setEncoding(originalCommit.getEncoding());
238 :
239 3 : return inserter.insert(cb);
240 : }
241 : }
|