Line data Source code
1 : // Copyright (C) 2014 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.MoreObjects.firstNonNull;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
20 :
21 : import com.google.auto.value.AutoValue;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.Account;
24 : import com.google.gerrit.entities.Change;
25 : import com.google.gerrit.entities.Comment;
26 : import com.google.gerrit.entities.HumanComment;
27 : import com.google.gerrit.entities.Project;
28 : import com.google.gerrit.entities.RefNames;
29 : import com.google.gerrit.exceptions.StorageException;
30 : import com.google.gerrit.server.GerritPersonIdent;
31 : import com.google.gerrit.server.config.AllUsersName;
32 : import com.google.inject.assistedinject.Assisted;
33 : import com.google.inject.assistedinject.AssistedInject;
34 : import java.io.IOException;
35 : import java.time.Instant;
36 : import java.util.ArrayList;
37 : import java.util.Arrays;
38 : import java.util.HashMap;
39 : import java.util.List;
40 : import java.util.Map;
41 : import org.eclipse.jgit.errors.ConfigInvalidException;
42 : import org.eclipse.jgit.lib.CommitBuilder;
43 : import org.eclipse.jgit.lib.ObjectId;
44 : import org.eclipse.jgit.lib.ObjectInserter;
45 : import org.eclipse.jgit.lib.PersonIdent;
46 : import org.eclipse.jgit.notes.NoteMap;
47 : import org.eclipse.jgit.revwalk.RevWalk;
48 :
49 : /**
50 : * A single delta to apply atomically to a change.
51 : *
52 : * <p>This delta contains only draft comments on a single patch set of a change by a single author.
53 : * This delta will become a single commit in the All-Users repository.
54 : *
55 : * <p>This class is not thread safe.
56 : */
57 : public class ChangeDraftUpdate extends AbstractChangeUpdate {
58 : public interface Factory {
59 : ChangeDraftUpdate create(
60 : ChangeNotes notes,
61 : @Assisted("effective") Account.Id accountId,
62 : @Assisted("real") Account.Id realAccountId,
63 : PersonIdent authorIdent,
64 : Instant when);
65 :
66 : ChangeDraftUpdate create(
67 : Change change,
68 : @Assisted("effective") Account.Id accountId,
69 : @Assisted("real") Account.Id realAccountId,
70 : PersonIdent authorIdent,
71 : Instant when);
72 : }
73 :
74 : @AutoValue
75 29 : abstract static class Key {
76 : abstract ObjectId commitId();
77 :
78 : abstract Comment.Key key();
79 : }
80 :
81 27 : enum DeleteReason {
82 27 : DELETED,
83 27 : PUBLISHED,
84 27 : FIXED
85 : }
86 :
87 : private static Key key(HumanComment c) {
88 29 : return new AutoValue_ChangeDraftUpdate_Key(c.getCommitId(), c.key);
89 : }
90 :
91 : private final AllUsersName draftsProject;
92 :
93 29 : private List<HumanComment> put = new ArrayList<>();
94 29 : private Map<Key, DeleteReason> delete = new HashMap<>();
95 :
96 : @SuppressWarnings("UnusedMethod")
97 : @AssistedInject
98 : private ChangeDraftUpdate(
99 : @GerritPersonIdent PersonIdent serverIdent,
100 : AllUsersName allUsers,
101 : ChangeNoteUtil noteUtil,
102 : @Assisted ChangeNotes notes,
103 : @Assisted("effective") Account.Id accountId,
104 : @Assisted("real") Account.Id realAccountId,
105 : @Assisted PersonIdent authorIdent,
106 : @Assisted Instant when) {
107 29 : super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
108 29 : this.draftsProject = allUsers;
109 29 : }
110 :
111 : @AssistedInject
112 : private ChangeDraftUpdate(
113 : @GerritPersonIdent PersonIdent serverIdent,
114 : AllUsersName allUsers,
115 : ChangeNoteUtil noteUtil,
116 : @Assisted Change change,
117 : @Assisted("effective") Account.Id accountId,
118 : @Assisted("real") Account.Id realAccountId,
119 : @Assisted PersonIdent authorIdent,
120 : @Assisted Instant when) {
121 26 : super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
122 26 : this.draftsProject = allUsers;
123 26 : }
124 :
125 : public void putComment(HumanComment c) {
126 21 : checkState(!put.contains(c), "comment already added");
127 21 : verifyComment(c);
128 21 : put.add(c);
129 21 : }
130 :
131 : /**
132 : * Marks a comment for deletion. Called when the comment is deleted because the user published it.
133 : */
134 : public void markCommentPublished(HumanComment c) {
135 26 : checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
136 26 : verifyComment(c);
137 26 : delete.put(key(c), DeleteReason.PUBLISHED);
138 26 : }
139 :
140 : /**
141 : * Marks a comment for deletion. Called when the comment is deleted because the user removed it.
142 : */
143 : public void deleteComment(HumanComment c) {
144 10 : checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
145 10 : verifyComment(c);
146 10 : delete.put(key(c), DeleteReason.DELETED);
147 10 : }
148 :
149 : /**
150 : * Marks a comment for deletion. Called when the comment should have been deleted previously, but
151 : * wasn't, so we're fixing it up.
152 : */
153 : public void deleteComment(ObjectId commitId, Comment.Key key) {
154 16 : Key commentKey = new AutoValue_ChangeDraftUpdate_Key(commitId, key);
155 16 : checkState(!delete.containsKey(commentKey), "comment already marked for deletion");
156 16 : delete.put(commentKey, DeleteReason.FIXED);
157 16 : }
158 :
159 : /**
160 : * Returns true if all we do in this operations is deletes caused by publishing or fixing up
161 : * comments.
162 : */
163 : public boolean canRunAsync() {
164 29 : return put.isEmpty()
165 27 : && delete.values().stream()
166 29 : .allMatch(r -> r == DeleteReason.PUBLISHED || r == DeleteReason.FIXED);
167 : }
168 :
169 : /**
170 : * Returns a copy of the current {@link ChangeDraftUpdate} that contains references to all
171 : * deletions. Copying of {@link ChangeDraftUpdate} is only allowed if it contains no new comments.
172 : */
173 : ChangeDraftUpdate copy() {
174 26 : checkState(
175 26 : put.isEmpty(),
176 : "copying ChangeDraftUpdate is allowed only if it doesn't contain new comments");
177 26 : ChangeDraftUpdate clonedUpdate =
178 : new ChangeDraftUpdate(
179 : authorIdent,
180 : draftsProject,
181 : noteUtil,
182 26 : new Change(getChange()),
183 : accountId,
184 : realAccountId,
185 : authorIdent,
186 : when);
187 26 : clonedUpdate.delete.putAll(delete);
188 26 : return clonedUpdate;
189 : }
190 :
191 : @Nullable
192 : private CommitBuilder storeCommentsInNotes(
193 : RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
194 : throws ConfigInvalidException, IOException {
195 29 : RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
196 29 : RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
197 :
198 29 : for (HumanComment c : put) {
199 21 : if (!delete.keySet().contains(key(c))) {
200 21 : cache.get(c.getCommitId()).putComment(c);
201 : }
202 21 : }
203 29 : for (Key k : delete.keySet()) {
204 27 : cache.get(k.commitId()).deleteComment(k.key());
205 27 : }
206 :
207 : // keyed by commit ID.
208 29 : Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
209 29 : boolean touchedAnyRevs = false;
210 29 : for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
211 29 : ObjectId id = e.getKey();
212 29 : byte[] data = e.getValue().build(noteUtil.getChangeNoteJson());
213 29 : if (!Arrays.equals(data, e.getValue().baseRaw)) {
214 21 : touchedAnyRevs = true;
215 : }
216 29 : if (data.length == 0) {
217 27 : rnm.noteMap.remove(id);
218 : } else {
219 21 : ObjectId dataBlob = ins.insert(OBJ_BLOB, data);
220 21 : rnm.noteMap.set(id, dataBlob);
221 : }
222 29 : }
223 :
224 : // If we didn't touch any notes, tell the caller this was a no-op update. We
225 : // couldn't have done this in isEmpty() below because we hadn't read the old
226 : // data yet.
227 29 : if (!touchedAnyRevs) {
228 21 : return NO_OP_UPDATE;
229 : }
230 :
231 : // If there are no comments left, tell the
232 : // caller to delete the entire ref.
233 21 : if (!rnm.noteMap.iterator().hasNext()) {
234 17 : return null;
235 : }
236 :
237 21 : ObjectId treeId = rnm.noteMap.writeTree(ins);
238 21 : cb.setTreeId(treeId);
239 21 : return cb;
240 : }
241 :
242 : private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
243 : throws ConfigInvalidException, IOException {
244 : // The old DraftCommentNotes already parsed the revision notes. We can reuse them as long as
245 : // the ref hasn't advanced.
246 29 : ChangeNotes changeNotes = getNotes();
247 29 : if (changeNotes != null) {
248 21 : DraftCommentNotes draftNotes = changeNotes.load().getDraftCommentNotes();
249 21 : if (draftNotes != null) {
250 9 : ObjectId idFromNotes = firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
251 9 : RevisionNoteMap<ChangeRevisionNote> rnm = draftNotes.getRevisionNoteMap();
252 9 : if (idFromNotes.equals(curr) && rnm != null) {
253 9 : return rnm;
254 : }
255 : }
256 : }
257 : NoteMap noteMap;
258 29 : if (!curr.equals(ObjectId.zeroId())) {
259 13 : noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
260 : } else {
261 29 : noteMap = NoteMap.newEmptyMap();
262 : }
263 : // Even though reading from changes might not be enabled, we need to
264 : // parse any existing revision notes so we can merge them.
265 29 : return RevisionNoteMap.parse(
266 29 : noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, HumanComment.Status.DRAFT);
267 : }
268 :
269 : @Override
270 : protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
271 : throws IOException {
272 29 : CommitBuilder cb = new CommitBuilder();
273 29 : cb.setMessage("Update draft comments");
274 : try {
275 29 : return storeCommentsInNotes(rw, ins, curr, cb);
276 0 : } catch (ConfigInvalidException e) {
277 0 : throw new StorageException(e);
278 : }
279 : }
280 :
281 : @Override
282 : protected Project.NameKey getProjectName() {
283 29 : return draftsProject;
284 : }
285 :
286 : @Override
287 : protected String getRefName() {
288 29 : return RefNames.refsDraftComments(getId(), accountId);
289 : }
290 :
291 : @Override
292 : protected void setParentCommit(CommitBuilder cb, ObjectId parentCommitId) {
293 21 : cb.setParentIds(); // Draft updates should not keep history of parent commits
294 21 : }
295 :
296 : @Override
297 : public boolean isEmpty() {
298 29 : return delete.isEmpty() && put.isEmpty();
299 : }
300 : }
|