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.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
19 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
20 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
21 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
22 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
23 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
24 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
25 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
26 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
27 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
28 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
29 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
30 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
31 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PRIVATE;
32 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
33 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REVERT_OF;
34 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_STATUS;
35 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBJECT;
36 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMISSION_ID;
37 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
38 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
39 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TOPIC;
40 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
41 : import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
42 : import static java.util.Comparator.comparing;
43 : import static java.util.Comparator.comparingInt;
44 : import static java.util.stream.Collectors.joining;
45 :
46 : import com.google.common.base.Enums;
47 : import com.google.common.base.Splitter;
48 : import com.google.common.collect.HashBasedTable;
49 : import com.google.common.collect.ImmutableListMultimap;
50 : import com.google.common.collect.ImmutableSet;
51 : import com.google.common.collect.ImmutableTable;
52 : import com.google.common.collect.ListMultimap;
53 : import com.google.common.collect.Lists;
54 : import com.google.common.collect.Maps;
55 : import com.google.common.collect.MultimapBuilder;
56 : import com.google.common.collect.Sets;
57 : import com.google.common.collect.Table;
58 : import com.google.common.collect.Tables;
59 : import com.google.common.flogger.FluentLogger;
60 : import com.google.common.primitives.Ints;
61 : import com.google.errorprone.annotations.FormatMethod;
62 : import com.google.gerrit.common.Nullable;
63 : import com.google.gerrit.entities.Account;
64 : import com.google.gerrit.entities.Address;
65 : import com.google.gerrit.entities.AttentionSetUpdate;
66 : import com.google.gerrit.entities.Change;
67 : import com.google.gerrit.entities.ChangeMessage;
68 : import com.google.gerrit.entities.HumanComment;
69 : import com.google.gerrit.entities.LabelId;
70 : import com.google.gerrit.entities.LabelType;
71 : import com.google.gerrit.entities.PatchSet;
72 : import com.google.gerrit.entities.PatchSetApproval;
73 : import com.google.gerrit.entities.RefNames;
74 : import com.google.gerrit.entities.SubmitRecord;
75 : import com.google.gerrit.entities.SubmitRecord.Label.Status;
76 : import com.google.gerrit.entities.SubmitRequirementResult;
77 : import com.google.gerrit.metrics.Timer0;
78 : import com.google.gerrit.server.AssigneeStatusUpdate;
79 : import com.google.gerrit.server.ReviewerByEmailSet;
80 : import com.google.gerrit.server.ReviewerSet;
81 : import com.google.gerrit.server.ReviewerStatusUpdate;
82 : import com.google.gerrit.server.account.externalids.ExternalIdCache;
83 : import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
84 : import com.google.gerrit.server.notedb.ChangeNotesParseApprovalUtil.ParsedPatchSetApproval;
85 : import com.google.gerrit.server.util.LabelVote;
86 : import java.io.IOException;
87 : import java.nio.charset.Charset;
88 : import java.sql.Timestamp;
89 : import java.time.Instant;
90 : import java.util.ArrayList;
91 : import java.util.Collection;
92 : import java.util.Collections;
93 : import java.util.HashMap;
94 : import java.util.HashSet;
95 : import java.util.Iterator;
96 : import java.util.LinkedHashMap;
97 : import java.util.List;
98 : import java.util.Map;
99 : import java.util.Objects;
100 : import java.util.Optional;
101 : import java.util.Set;
102 : import java.util.TreeSet;
103 : import java.util.function.Function;
104 : import java.util.stream.Collectors;
105 : import org.eclipse.jgit.errors.ConfigInvalidException;
106 : import org.eclipse.jgit.errors.InvalidObjectIdException;
107 : import org.eclipse.jgit.lib.ObjectId;
108 : import org.eclipse.jgit.lib.ObjectReader;
109 : import org.eclipse.jgit.lib.PersonIdent;
110 : import org.eclipse.jgit.notes.NoteMap;
111 : import org.eclipse.jgit.revwalk.FooterKey;
112 : import org.eclipse.jgit.util.RawParseUtils;
113 :
114 : /**
115 : * Parses {@link ChangeNotesState} out of the change meta ref.
116 : *
117 : * <p>NOTE: all changes to the change notes storage format must be both forward and backward
118 : * compatible, i.e.:
119 : *
120 : * <ul>
121 : * <li>The server, running the new binary version must be able to parse the data, written by the
122 : * previous binary version.
123 : * <li>The server, running the old binary version must be able to parse the data, written by the
124 : * new binary version.
125 : * </ul>
126 : *
127 : * <p>Thus, when introducing storage format update, the following procedure must be used:
128 : *
129 : * <ol>
130 : * <li>The read path ({@link ChangeNotesParser}) needs to be updated to handle both the old and
131 : * the new data format.
132 : * <li>In a separate change, the write path (e.g. {@link ChangeUpdate}, {@link ChangeNoteJson}) is
133 : * updated to write the new format, guarded by {@link
134 : * com.google.gerrit.server.experiments.ExperimentFeatures} flag, if possible.
135 : * <li>Once the 'read' change is roll out and is roll back safe, the 'write' change can be
136 : * submitted/the experiment flag can be flipped.
137 : * </ol>
138 : */
139 : class ChangeNotesParser {
140 103 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
141 :
142 103 : private static final Splitter RULE_SPLITTER = Splitter.on(": ");
143 103 : private static final Splitter HASHTAG_SPLITTER = Splitter.on(",");
144 :
145 : // Private final members initialized in the constructor.
146 : private final ChangeNoteJson changeNoteJson;
147 : private final NoteDbMetrics metrics;
148 : private final Change.Id id;
149 : private final ObjectId tip;
150 : private final ChangeNotesRevWalk walk;
151 :
152 : // Private final but mutable members initialized in the constructor and filled
153 : // in during the parsing process.
154 : private final Table<Account.Id, ReviewerStateInternal, Instant> reviewers;
155 : private final Table<Address, ReviewerStateInternal, Instant> reviewersByEmail;
156 : private final List<Account.Id> allPastReviewers;
157 : private final List<ReviewerStatusUpdate> reviewerUpdates;
158 : /** Holds only the most recent update per user. Older updates are discarded. */
159 : private final Map<Account.Id, AttentionSetUpdate> latestAttentionStatus;
160 : /** Holds all updates to attention set. */
161 : private final List<AttentionSetUpdate> allAttentionSetUpdates;
162 :
163 : private final List<AssigneeStatusUpdate> assigneeUpdates;
164 : private final List<SubmitRecord> submitRecords;
165 : private final ListMultimap<ObjectId, HumanComment> humanComments;
166 : private final List<SubmitRequirementResult> submitRequirementResults;
167 : private final Map<PatchSet.Id, PatchSet.Builder> patchSets;
168 : private final Set<PatchSet.Id> deletedPatchSets;
169 : private final Map<PatchSet.Id, PatchSetState> patchSetStates;
170 : private final List<PatchSet.Id> currentPatchSets;
171 : private final Map<PatchSetApproval.Key, PatchSetApproval.Builder> approvals;
172 : private final List<PatchSetApproval.Builder> bufferedApprovals;
173 : private final List<ChangeMessage> allChangeMessages;
174 :
175 : // Non-final private members filled in during the parsing process.
176 : private String branch;
177 : private Change.Status status;
178 : private String topic;
179 : private Set<String> hashtags;
180 : private Instant createdOn;
181 : private Instant lastUpdatedOn;
182 : private Account.Id ownerId;
183 : private String serverId;
184 : private String changeId;
185 : private String subject;
186 : private String originalSubject;
187 : private String submissionId;
188 : private String tag;
189 : private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
190 : private Boolean isPrivate;
191 : private Boolean workInProgress;
192 : private Boolean previousWorkInProgressFooter;
193 : private Boolean hasReviewStarted;
194 : private ReviewerSet pendingReviewers;
195 : private ReviewerByEmailSet pendingReviewersByEmail;
196 : private Change.Id revertOf;
197 : private int updateCount;
198 : // Null indicates that the field was not parsed (yet).
199 : // We only set the value once, based on the latest update (the actual value or Optional.empty() if
200 : // the latest record unsets the field).
201 : private Optional<PatchSet.Id> cherryPickOf;
202 : private Instant mergedOn;
203 : private final ExternalIdCache externalIdCache;
204 : private final String gerritServerId;
205 :
206 : ChangeNotesParser(
207 : Change.Id changeId,
208 : ObjectId tip,
209 : ChangeNotesRevWalk walk,
210 : ChangeNoteJson changeNoteJson,
211 : NoteDbMetrics metrics,
212 : String gerritServerId,
213 103 : ExternalIdCache externalIdCache) {
214 103 : this.id = changeId;
215 103 : this.tip = tip;
216 103 : this.walk = walk;
217 103 : this.changeNoteJson = changeNoteJson;
218 103 : this.metrics = metrics;
219 103 : this.externalIdCache = externalIdCache;
220 103 : this.gerritServerId = gerritServerId;
221 103 : approvals = new LinkedHashMap<>();
222 103 : bufferedApprovals = new ArrayList<>();
223 103 : reviewers = HashBasedTable.create();
224 103 : reviewersByEmail = HashBasedTable.create();
225 103 : pendingReviewers = ReviewerSet.empty();
226 103 : pendingReviewersByEmail = ReviewerByEmailSet.empty();
227 103 : allPastReviewers = new ArrayList<>();
228 103 : reviewerUpdates = new ArrayList<>();
229 103 : latestAttentionStatus = new HashMap<>();
230 103 : allAttentionSetUpdates = new ArrayList<>();
231 103 : assigneeUpdates = new ArrayList<>();
232 103 : submitRecords = Lists.newArrayListWithExpectedSize(1);
233 103 : allChangeMessages = new ArrayList<>();
234 103 : humanComments = MultimapBuilder.hashKeys().arrayListValues().build();
235 103 : submitRequirementResults = new ArrayList<>();
236 103 : patchSets = new HashMap<>();
237 103 : deletedPatchSets = new HashSet<>();
238 103 : patchSetStates = new HashMap<>();
239 103 : currentPatchSets = new ArrayList<>();
240 103 : }
241 :
242 : ChangeNotesState parseAll() throws ConfigInvalidException, IOException {
243 : // Don't include initial parse in timer, as this might do more I/O to page
244 : // in the block containing most commits. Later reads are not guaranteed to
245 : // avoid I/O, but often should.
246 103 : walk.reset();
247 103 : walk.markStart(walk.parseCommit(tip));
248 :
249 103 : try (Timer0.Context timer = metrics.parseLatency.start()) {
250 : ChangeNotesCommit commit;
251 103 : while ((commit = walk.next()) != null) {
252 103 : parse(commit);
253 : }
254 103 : if (hasReviewStarted == null) {
255 22 : if (previousWorkInProgressFooter == null) {
256 2 : hasReviewStarted = true;
257 : } else {
258 21 : hasReviewStarted = !previousWorkInProgressFooter;
259 : }
260 : }
261 103 : parseNotes();
262 103 : allPastReviewers.addAll(reviewers.rowKeySet());
263 103 : pruneReviewers();
264 103 : pruneReviewersByEmail();
265 :
266 103 : updatePatchSetStates();
267 103 : checkMandatoryFooters();
268 : }
269 :
270 103 : return buildState();
271 : }
272 :
273 : RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() {
274 103 : return revisionNoteMap;
275 : }
276 :
277 : private ChangeNotesState buildState() throws ConfigInvalidException {
278 103 : return ChangeNotesState.create(
279 103 : tip.copy(),
280 : id,
281 103 : Change.key(changeId),
282 : createdOn,
283 : lastUpdatedOn,
284 : ownerId,
285 : serverId,
286 : branch,
287 103 : buildCurrentPatchSetId(),
288 : subject,
289 : topic,
290 : originalSubject,
291 : submissionId,
292 : status,
293 103 : firstNonNull(hashtags, ImmutableSet.of()),
294 103 : buildPatchSets(),
295 103 : buildApprovals(),
296 103 : ReviewerSet.fromTable(Tables.transpose(reviewers)),
297 103 : ReviewerByEmailSet.fromTable(Tables.transpose(reviewersByEmail)),
298 : pendingReviewers,
299 : pendingReviewersByEmail,
300 : allPastReviewers,
301 103 : buildReviewerUpdates(),
302 103 : ImmutableSet.copyOf(latestAttentionStatus.values()),
303 : allAttentionSetUpdates,
304 : assigneeUpdates,
305 : submitRecords,
306 103 : buildAllMessages(),
307 : humanComments,
308 : submitRequirementResults,
309 103 : firstNonNull(isPrivate, false),
310 103 : firstNonNull(workInProgress, false),
311 103 : firstNonNull(hasReviewStarted, true),
312 : revertOf,
313 103 : cherryPickOf != null ? cherryPickOf.orElse(null) : null,
314 : updateCount,
315 : mergedOn);
316 : }
317 :
318 : private Map<PatchSet.Id, PatchSet> buildPatchSets() throws ConfigInvalidException {
319 103 : Map<PatchSet.Id, PatchSet> result = Maps.newHashMapWithExpectedSize(patchSets.size());
320 103 : for (Map.Entry<PatchSet.Id, PatchSet.Builder> e : patchSets.entrySet()) {
321 : try {
322 103 : PatchSet ps = e.getValue().build();
323 103 : result.put(ps.id(), ps);
324 0 : } catch (Exception ex) {
325 0 : ConfigInvalidException cie = parseException("Error building patch set %s", e.getKey());
326 0 : cie.initCause(ex);
327 0 : throw cie;
328 103 : }
329 103 : }
330 103 : return result;
331 : }
332 :
333 : @Nullable
334 : private PatchSet.Id buildCurrentPatchSetId() {
335 : // currentPatchSets are in parse order, i.e. newest first. Pick the first
336 : // patch set that was marked as current, excluding deleted patch sets.
337 103 : for (PatchSet.Id psId : currentPatchSets) {
338 103 : if (patchSetCommitParsed(psId)) {
339 103 : return psId;
340 : }
341 0 : }
342 1 : return null;
343 : }
344 :
345 : private ListMultimap<PatchSet.Id, PatchSetApproval> buildApprovals() {
346 : ListMultimap<PatchSet.Id, PatchSetApproval> result =
347 103 : MultimapBuilder.hashKeys().arrayListValues().build();
348 103 : for (PatchSetApproval.Builder a : approvals.values()) {
349 68 : if (!patchSetCommitParsed(a.key().patchSetId())) {
350 0 : continue; // Patch set deleted or missing.
351 68 : } else if (allPastReviewers.contains(a.key().accountId())
352 66 : && !reviewers.containsRow(a.key().accountId())) {
353 5 : continue; // Reviewer was explicitly removed.
354 : }
355 68 : result.put(a.key().patchSetId(), a.build());
356 68 : }
357 103 : if (status != null && status.isClosed() && !isAnyApprovalCopied(result)) {
358 : // If the change is closed, check if there are "submit records" with approvals that do not
359 : // exist on the latest patch-set and copy them to the latest patch-set.
360 : // We do not invoke this logic if any approval is copied. This is because prior to change
361 : // https://gerrit-review.googlesource.com/c/gerrit/+/318135 we used to copy approvals
362 : // dynamically (e.g. when requesting the change page). After that change, we started
363 : // persisting copied votes in NoteDb, so we don't need to do this back-filling.
364 : // Prior to that change (318135), we could've had changes with dynamically copied approvals
365 : // that were merged in NoteDb but these approvals do not exist on the latest patch-set, so
366 : // we need to back-fill these approvals.
367 58 : PatchSet.Id latestPs = buildCurrentPatchSetId();
368 58 : backFillMissingCopiedApprovalsFromSubmitRecords(result, latestPs).stream()
369 58 : .forEach(a -> result.put(latestPs, a));
370 : }
371 103 : result.keySet().forEach(k -> result.get(k).sort(ChangeNotes.PSA_BY_TIME));
372 103 : return result;
373 : }
374 :
375 : /**
376 : * Returns patch-set approvals that do not exist on the latest patch-set but for which a submit
377 : * record exists in NoteDb when the change was merged.
378 : */
379 : private List<PatchSetApproval> backFillMissingCopiedApprovalsFromSubmitRecords(
380 : ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals, @Nullable PatchSet.Id latestPs) {
381 58 : List<PatchSetApproval> copiedApprovals = new ArrayList<>();
382 58 : if (latestPs == null) {
383 0 : return copiedApprovals;
384 : }
385 58 : List<PatchSetApproval> approvalsOnLatestPs = allApprovals.get(latestPs);
386 58 : ListMultimap<Account.Id, PatchSetApproval> approvalsByUser = getApprovalsByUser(allApprovals);
387 58 : List<SubmitRecord.Label> submitRecordLabels =
388 58 : submitRecords.stream()
389 58 : .filter(r -> r.labels != null)
390 58 : .flatMap(r -> r.labels.stream())
391 58 : .filter(label -> Status.OK.equals(label.status) || Status.MAY.equals(label.status))
392 58 : .collect(Collectors.toList());
393 58 : for (SubmitRecord.Label recordLabel : submitRecordLabels) {
394 48 : String labelName = recordLabel.label;
395 48 : Account.Id appliedBy = recordLabel.appliedBy;
396 48 : if (appliedBy == null || labelName == null) {
397 0 : continue;
398 : }
399 48 : boolean existsAtLatestPs =
400 48 : approvalsOnLatestPs.stream()
401 48 : .anyMatch(a -> a.accountId().equals(appliedBy) && a.label().equals(labelName));
402 48 : if (existsAtLatestPs) {
403 47 : continue;
404 : }
405 : // Search for an approval for this label on the max previous patch-set and copy the approval.
406 5 : Collection<PatchSetApproval> userApprovals =
407 5 : approvalsByUser.get(appliedBy).stream()
408 5 : .filter(approval -> approval.label().equals(labelName))
409 5 : .collect(Collectors.toList());
410 5 : if (userApprovals.isEmpty()) {
411 4 : continue;
412 : }
413 1 : PatchSetApproval lastApproved =
414 1 : Collections.max(userApprovals, comparingInt(a -> a.patchSetId().get()));
415 1 : copiedApprovals.add(lastApproved.copyWithPatchSet(latestPs));
416 1 : }
417 58 : return copiedApprovals;
418 : }
419 :
420 : private boolean isAnyApprovalCopied(ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
421 58 : return allApprovals.values().stream().anyMatch(approval -> approval.copied());
422 : }
423 :
424 : private ListMultimap<Account.Id, PatchSetApproval> getApprovalsByUser(
425 : ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
426 58 : return allApprovals.values().stream()
427 58 : .collect(
428 58 : ImmutableListMultimap.toImmutableListMultimap(
429 58 : PatchSetApproval::accountId, Function.identity()));
430 : }
431 :
432 : private List<ReviewerStatusUpdate> buildReviewerUpdates() {
433 103 : List<ReviewerStatusUpdate> result = new ArrayList<>();
434 103 : HashMap<Account.Id, ReviewerStateInternal> lastState = new HashMap<>();
435 103 : for (ReviewerStatusUpdate u : Lists.reverse(reviewerUpdates)) {
436 75 : if (!Objects.equals(ownerId, u.reviewer()) && lastState.get(u.reviewer()) != u.state()) {
437 49 : result.add(u);
438 49 : lastState.put(u.reviewer(), u.state());
439 : }
440 75 : }
441 103 : return result;
442 : }
443 :
444 : private List<ChangeMessage> buildAllMessages() {
445 103 : return Lists.reverse(allChangeMessages);
446 : }
447 :
448 : private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
449 103 : Instant commitTimestamp = getCommitTimestamp(commit);
450 :
451 103 : createdOn = commitTimestamp;
452 103 : parseTag(commit);
453 :
454 103 : if (branch == null) {
455 103 : branch = parseBranch(commit);
456 : }
457 :
458 103 : PatchSet.Id psId = parsePatchSetId(commit);
459 103 : PatchSetState psState = parsePatchSetState(commit);
460 103 : if (psState != null) {
461 2 : if (!patchSetStates.containsKey(psId)) {
462 2 : patchSetStates.put(psId, psState);
463 : }
464 2 : if (psState == PatchSetState.DELETED) {
465 2 : deletedPatchSets.add(psId);
466 : }
467 : }
468 :
469 103 : Account.Id accountId = parseIdent(commit);
470 103 : if (accountId != null) {
471 103 : ownerId = accountId;
472 103 : PersonIdent personIdent = commit.getAuthorIdent();
473 103 : serverId = NoteDbUtil.extractHostPartFromPersonIdent(personIdent);
474 103 : } else {
475 2 : serverId = "UNKNOWN_SERVER_ID";
476 : }
477 103 : Account.Id realAccountId = parseRealAccountId(commit, accountId);
478 :
479 103 : if (changeId == null) {
480 103 : changeId = parseChangeId(commit);
481 : }
482 :
483 103 : String currSubject = parseSubject(commit);
484 103 : if (currSubject != null) {
485 103 : if (subject == null) {
486 103 : subject = currSubject;
487 : }
488 103 : originalSubject = currSubject;
489 : }
490 :
491 103 : boolean hasChangeMessage =
492 103 : parseChangeMessage(psId, accountId, realAccountId, commit, commitTimestamp);
493 103 : if (topic == null) {
494 103 : topic = parseTopic(commit);
495 : }
496 :
497 103 : parseHashtags(commit);
498 103 : parseAttentionSetUpdates(commit);
499 103 : parseAssigneeUpdates(commitTimestamp, commit);
500 :
501 103 : parseSubmission(commit, commitTimestamp);
502 :
503 103 : if (lastUpdatedOn == null || commitTimestamp.isAfter(lastUpdatedOn)) {
504 103 : lastUpdatedOn = commitTimestamp;
505 : }
506 :
507 103 : if (deletedPatchSets.contains(psId)) {
508 : // Do not update PS details as PS was deleted and this meta data is of no relevance.
509 2 : return;
510 : }
511 :
512 : // Parse mutable patch set fields first so they can be recorded in the PendingPatchSetFields.
513 103 : parseDescription(psId, commit);
514 103 : parseGroups(psId, commit);
515 :
516 103 : ObjectId currRev = parseRevision(commit);
517 103 : if (currRev != null) {
518 103 : parsePatchSet(psId, currRev, accountId, commitTimestamp);
519 : }
520 103 : parseCurrentPatchSet(psId, commit);
521 :
522 103 : if (status == null) {
523 103 : status = parseStatus(commit);
524 : }
525 :
526 : // Parse approvals after status to treat approvals in the same commit as
527 : // "Status: merged" as non-post-submit.
528 103 : for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
529 68 : parseApproval(psId, accountId, realAccountId, commitTimestamp, line);
530 68 : }
531 103 : for (String line : commit.getFooterLineValues(FOOTER_COPIED_LABEL)) {
532 13 : parseCopiedApproval(psId, commitTimestamp, line);
533 13 : }
534 :
535 103 : for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
536 103 : for (String line : commit.getFooterLineValues(state.getFooterKey())) {
537 75 : parseReviewer(commitTimestamp, state, line);
538 75 : }
539 103 : for (String line : commit.getFooterLineValues(state.getByEmailFooterKey())) {
540 11 : parseReviewerByEmail(commitTimestamp, state, line);
541 11 : }
542 : // Don't update timestamp when a reviewer was added, matching RevewDb
543 : // behavior.
544 : }
545 :
546 103 : if (isPrivate == null) {
547 103 : parseIsPrivate(commit);
548 : }
549 :
550 103 : if (revertOf == null) {
551 103 : revertOf = parseRevertOf(commit);
552 : }
553 :
554 103 : if (cherryPickOf == null) {
555 103 : cherryPickOf = parseCherryPickOf(commit);
556 : }
557 :
558 103 : previousWorkInProgressFooter = null;
559 103 : parseWorkInProgress(commit);
560 103 : if (countTowardsMaxUpdatesLimit(commit, hasChangeMessage)) {
561 103 : updateCount++;
562 : }
563 103 : }
564 :
565 : private void parseSubmission(ChangeNotesCommit commit, Instant commitTimestamp)
566 : throws ConfigInvalidException {
567 : // Only parse the most recent sumbit commit (there should be exactly one).
568 103 : if (submissionId == null) {
569 103 : submissionId = parseSubmissionId(commit);
570 : }
571 :
572 103 : if (submissionId != null && mergedOn == null) {
573 57 : mergedOn = commitTimestamp;
574 : }
575 :
576 103 : if (submitRecords.isEmpty()) {
577 : // Only parse the most recent set of submit records; any older ones are
578 : // still there, but not currently used.
579 103 : parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
580 : }
581 103 : }
582 :
583 : private String parseSubmissionId(ChangeNotesCommit commit) throws ConfigInvalidException {
584 103 : return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
585 : }
586 :
587 : @Nullable
588 : private String parseBranch(ChangeNotesCommit commit) throws ConfigInvalidException {
589 103 : String branch = parseOneFooter(commit, FOOTER_BRANCH);
590 103 : return branch != null ? RefNames.fullName(branch) : null;
591 : }
592 :
593 : private String parseChangeId(ChangeNotesCommit commit) throws ConfigInvalidException {
594 103 : return parseOneFooter(commit, FOOTER_CHANGE_ID);
595 : }
596 :
597 : private String parseSubject(ChangeNotesCommit commit) throws ConfigInvalidException {
598 103 : return parseOneFooter(commit, FOOTER_SUBJECT);
599 : }
600 :
601 : private Account.Id parseRealAccountId(ChangeNotesCommit commit, Account.Id effectiveAccountId)
602 : throws ConfigInvalidException {
603 103 : String realUser = parseOneFooter(commit, FOOTER_REAL_USER);
604 103 : if (realUser == null) {
605 103 : return effectiveAccountId;
606 : }
607 4 : PersonIdent ident = RawParseUtils.parsePersonIdent(realUser);
608 4 : return parseIdent(ident);
609 : }
610 :
611 : private String parseTopic(ChangeNotesCommit commit) throws ConfigInvalidException {
612 103 : return parseOneFooter(commit, FOOTER_TOPIC);
613 : }
614 :
615 : @Nullable
616 : private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
617 : throws ConfigInvalidException {
618 103 : List<String> footerLines = commit.getFooterLineValues(footerKey);
619 103 : if (footerLines.isEmpty()) {
620 103 : return null;
621 103 : } else if (footerLines.size() > 1) {
622 1 : throw expectedOneFooter(footerKey, footerLines);
623 : }
624 103 : return footerLines.get(0);
625 : }
626 :
627 : private String parseExactlyOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
628 : throws ConfigInvalidException {
629 103 : String line = parseOneFooter(commit, footerKey);
630 103 : if (line == null) {
631 1 : throw expectedOneFooter(footerKey, Collections.emptyList());
632 : }
633 103 : return line;
634 : }
635 :
636 : @Nullable
637 : private ObjectId parseRevision(ChangeNotesCommit commit) throws ConfigInvalidException {
638 103 : String sha = parseOneFooter(commit, FOOTER_COMMIT);
639 103 : if (sha == null) {
640 82 : return null;
641 : }
642 : try {
643 103 : return ObjectId.fromString(sha);
644 1 : } catch (InvalidObjectIdException e) {
645 1 : ConfigInvalidException cie = invalidFooter(FOOTER_COMMIT, sha);
646 1 : cie.initCause(e);
647 1 : throw cie;
648 : }
649 : }
650 :
651 : private void parsePatchSet(PatchSet.Id psId, ObjectId rev, Account.Id accountId, Instant ts)
652 : throws ConfigInvalidException {
653 103 : if (accountId == null) {
654 0 : throw parseException("patch set %s requires an identified user as uploader", psId.get());
655 : }
656 103 : if (patchSetCommitParsed(psId)) {
657 1 : ObjectId commitId = patchSets.get(psId).commitId().orElseThrow(IllegalStateException::new);
658 1 : throw new ConfigInvalidException(
659 1 : String.format(
660 : "Multiple revisions parsed for patch set %s: %s and %s",
661 1 : psId.get(), commitId.name(), rev.name()));
662 : }
663 103 : patchSets
664 103 : .computeIfAbsent(psId, id -> PatchSet.builder())
665 103 : .id(psId)
666 103 : .commitId(rev)
667 103 : .uploader(accountId)
668 103 : .createdOn(ts);
669 : // Fields not set here:
670 : // * Groups, parsed earlier in parseGroups.
671 : // * Description, parsed earlier in parseDescription.
672 : // * Push certificate, parsed later in parseNotes.
673 103 : }
674 :
675 : private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
676 : throws ConfigInvalidException {
677 103 : String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
678 103 : if (groupsStr == null) {
679 82 : return;
680 : }
681 103 : checkPatchSetCommitNotParsed(psId, FOOTER_GROUPS);
682 103 : PatchSet.Builder pending = patchSets.computeIfAbsent(psId, id -> PatchSet.builder());
683 103 : if (pending.groups().isEmpty()) {
684 103 : pending.groups(PatchSet.splitGroups(groupsStr));
685 : }
686 103 : }
687 :
688 : private void parseCurrentPatchSet(PatchSet.Id psId, ChangeNotesCommit commit)
689 : throws ConfigInvalidException {
690 : // This commit implies a new current patch set if either it creates a new
691 : // patch set, or sets the current field explicitly.
692 103 : boolean current = false;
693 103 : if (parseOneFooter(commit, FOOTER_COMMIT) != null) {
694 103 : current = true;
695 : } else {
696 82 : String currentStr = parseOneFooter(commit, FOOTER_CURRENT);
697 82 : if (Boolean.TRUE.toString().equalsIgnoreCase(currentStr)) {
698 11 : current = true;
699 82 : } else if (currentStr != null) {
700 : // Only "true" is allowed; unsetting the current patch set makes no
701 : // sense.
702 1 : throw invalidFooter(FOOTER_CURRENT, currentStr);
703 : }
704 : }
705 103 : if (current) {
706 103 : currentPatchSets.add(psId);
707 : }
708 103 : }
709 :
710 : private void parseHashtags(ChangeNotesCommit commit) throws ConfigInvalidException {
711 : // Commits are parsed in reverse order and only the last set of hashtags
712 : // should be used.
713 103 : if (hashtags != null) {
714 9 : return;
715 : }
716 103 : List<String> hashtagsLines = commit.getFooterLineValues(FOOTER_HASHTAGS);
717 103 : if (hashtagsLines.isEmpty()) {
718 103 : return;
719 9 : } else if (hashtagsLines.size() > 1) {
720 0 : throw expectedOneFooter(FOOTER_HASHTAGS, hashtagsLines);
721 9 : } else if (hashtagsLines.get(0).isEmpty()) {
722 1 : hashtags = ImmutableSet.of();
723 : } else {
724 9 : hashtags = Sets.newHashSet(HASHTAG_SPLITTER.split(hashtagsLines.get(0)));
725 : }
726 9 : }
727 :
728 : private void parseAttentionSetUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
729 103 : List<String> attentionStrings = commit.getFooterLineValues(FOOTER_ATTENTION);
730 103 : for (String attentionString : attentionStrings) {
731 :
732 52 : Optional<AttentionSetUpdate> attentionStatus =
733 52 : ChangeNoteUtil.attentionStatusFromJson(
734 52 : Instant.ofEpochSecond(commit.getCommitTime()), attentionString);
735 52 : if (!attentionStatus.isPresent()) {
736 0 : throw invalidFooter(FOOTER_ATTENTION, attentionString);
737 : }
738 : // Processing is in reverse chronological order. Keep only the latest update.
739 52 : latestAttentionStatus.putIfAbsent(attentionStatus.get().account(), attentionStatus.get());
740 :
741 : // Keep all updates as well.
742 52 : allAttentionSetUpdates.add(attentionStatus.get());
743 52 : }
744 103 : }
745 :
746 : private void parseAssigneeUpdates(Instant ts, ChangeNotesCommit commit)
747 : throws ConfigInvalidException {
748 103 : String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
749 103 : if (assigneeValue != null) {
750 : Optional<Account.Id> parsedAssignee;
751 7 : if (assigneeValue.equals("")) {
752 : // Empty footer found, assignee deleted
753 2 : parsedAssignee = Optional.empty();
754 : } else {
755 7 : PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
756 7 : parsedAssignee = Optional.ofNullable(parseIdent(ident));
757 : }
758 7 : assigneeUpdates.add(AssigneeStatusUpdate.create(ts, ownerId, parsedAssignee));
759 : }
760 103 : }
761 :
762 : private void parseTag(ChangeNotesCommit commit) throws ConfigInvalidException {
763 103 : tag = null;
764 103 : List<String> tagLines = commit.getFooterLineValues(FOOTER_TAG);
765 103 : if (tagLines.isEmpty()) {
766 73 : return;
767 103 : } else if (tagLines.size() == 1) {
768 103 : tag = tagLines.get(0);
769 : } else {
770 1 : throw expectedOneFooter(FOOTER_TAG, tagLines);
771 : }
772 103 : }
773 :
774 : @Nullable
775 : private Change.Status parseStatus(ChangeNotesCommit commit) throws ConfigInvalidException {
776 103 : List<String> statusLines = commit.getFooterLineValues(FOOTER_STATUS);
777 103 : if (statusLines.isEmpty()) {
778 86 : return null;
779 103 : } else if (statusLines.size() > 1) {
780 1 : throw expectedOneFooter(FOOTER_STATUS, statusLines);
781 : }
782 103 : Change.Status status =
783 103 : Enums.getIfPresent(Change.Status.class, statusLines.get(0).toUpperCase()).orNull();
784 103 : if (status == null) {
785 1 : throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
786 : }
787 : // All approvals after MERGED and before the next status change get the postSubmit
788 : // bit. (Currently the state can't change from MERGED to something else, but just in case.) The
789 : // exception is the legacy SUBM approval, which is never considered post-submit, but might end
790 : // up sorted after the submit during rebuilding.
791 103 : if (status == Change.Status.MERGED) {
792 57 : for (PatchSetApproval.Builder psa : bufferedApprovals) {
793 4 : if (!psa.key().isLegacySubmit()) {
794 4 : psa.postSubmit(true);
795 : }
796 4 : }
797 : }
798 103 : bufferedApprovals.clear();
799 103 : return status;
800 : }
801 :
802 : private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit) throws ConfigInvalidException {
803 103 : String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
804 103 : int s = psIdLine.indexOf(' ');
805 103 : String psIdStr = s < 0 ? psIdLine : psIdLine.substring(0, s);
806 103 : Integer psId = Ints.tryParse(psIdStr);
807 103 : if (psId == null) {
808 1 : throw invalidFooter(FOOTER_PATCH_SET, psIdStr);
809 : }
810 103 : return PatchSet.id(id, psId);
811 : }
812 :
813 : @Nullable
814 : private PatchSetState parsePatchSetState(ChangeNotesCommit commit) throws ConfigInvalidException {
815 103 : String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
816 103 : int s = psIdLine.indexOf(' ');
817 103 : if (s < 0) {
818 103 : return null;
819 : }
820 2 : String withParens = psIdLine.substring(s + 1);
821 2 : if (withParens.startsWith("(") && withParens.endsWith(")")) {
822 2 : PatchSetState state =
823 2 : Enums.getIfPresent(
824 : PatchSetState.class,
825 2 : withParens.substring(1, withParens.length() - 1).toUpperCase())
826 2 : .orNull();
827 2 : if (state != null) {
828 2 : return state;
829 : }
830 : }
831 1 : throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
832 : }
833 :
834 : private void parseDescription(PatchSet.Id psId, ChangeNotesCommit commit)
835 : throws ConfigInvalidException {
836 103 : List<String> descLines = commit.getFooterLineValues(FOOTER_PATCH_SET_DESCRIPTION);
837 103 : if (descLines.isEmpty()) {
838 103 : return;
839 : }
840 :
841 24 : checkPatchSetCommitNotParsed(psId, FOOTER_PATCH_SET_DESCRIPTION);
842 24 : if (descLines.size() == 1) {
843 24 : String desc = descLines.get(0).trim();
844 24 : PatchSet.Builder pending = patchSets.computeIfAbsent(psId, p -> PatchSet.builder());
845 24 : if (!pending.description().isPresent()) {
846 24 : pending.description(Optional.of(desc));
847 : }
848 24 : } else {
849 0 : throw expectedOneFooter(FOOTER_PATCH_SET_DESCRIPTION, descLines);
850 : }
851 24 : }
852 :
853 : private boolean parseChangeMessage(
854 : PatchSet.Id psId,
855 : Account.Id accountId,
856 : Account.Id realAccountId,
857 : ChangeNotesCommit commit,
858 : Instant ts) {
859 103 : Optional<String> changeMsgString = getChangeMessageString(commit);
860 103 : if (!changeMsgString.isPresent()) {
861 44 : return false;
862 : }
863 :
864 103 : ChangeMessage changeMessage =
865 103 : ChangeMessage.create(
866 103 : ChangeMessage.key(psId.changeId(), commit.name()),
867 : accountId,
868 : ts,
869 : psId,
870 103 : changeMsgString.get(),
871 : realAccountId,
872 : tag);
873 103 : return allChangeMessages.add(changeMessage);
874 : }
875 :
876 : public static Optional<String> getChangeMessageString(ChangeNotesCommit commit) {
877 103 : byte[] raw = commit.getRawBuffer();
878 103 : Charset enc = RawParseUtils.parseEncoding(raw);
879 :
880 103 : Optional<ChangeNoteUtil.CommitMessageRange> range = parseCommitMessageRange(commit);
881 103 : return range.map(
882 : commitMessageRange ->
883 103 : commitMessageRange.hasChangeMessage()
884 103 : ? RawParseUtils.decode(
885 : enc,
886 : raw,
887 103 : commitMessageRange.changeMessageStart(),
888 103 : commitMessageRange.changeMessageEnd() + 1)
889 44 : : null);
890 : }
891 :
892 : private void parseNotes() throws IOException, ConfigInvalidException {
893 103 : ObjectReader reader = walk.getObjectReader();
894 103 : ChangeNotesCommit tipCommit = walk.parseCommit(tip);
895 103 : revisionNoteMap =
896 103 : RevisionNoteMap.parse(
897 103 : changeNoteJson, reader, NoteMap.read(reader, tipCommit), HumanComment.Status.PUBLISHED);
898 103 : Map<ObjectId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
899 :
900 103 : for (Map.Entry<ObjectId, ChangeRevisionNote> e : rns.entrySet()) {
901 64 : for (HumanComment c : e.getValue().getEntities()) {
902 26 : humanComments.put(e.getKey(), c);
903 26 : }
904 64 : }
905 :
906 : // Lookup submit requirement results from the revision notes of the last PS that has stored
907 : // submit requirements. This is important for cases where the change was abandoned/un-abandoned
908 : // multiple times. With each abandon, we store submit requirement results in NoteDb, so we can
909 : // end up having stored SRs in many revision notes. We should only return SRs from the last
910 : // PS of them.
911 : for (PatchSet.Builder ps :
912 103 : patchSets.values().stream()
913 103 : .sorted(comparingInt((PatchSet.Builder p) -> p.id().get()).reversed())
914 103 : .collect(Collectors.toList())) {
915 103 : Optional<ObjectId> maybePsCommitId = ps.commitId();
916 103 : if (!maybePsCommitId.isPresent()) {
917 0 : continue;
918 : }
919 103 : ObjectId psCommitId = maybePsCommitId.get();
920 103 : if (rns.containsKey(psCommitId)
921 64 : && rns.get(psCommitId).getSubmitRequirementsResult() != null) {
922 54 : rns.get(psCommitId)
923 54 : .getSubmitRequirementsResult()
924 54 : .forEach(sr -> submitRequirementResults.add(sr));
925 54 : break;
926 : }
927 103 : }
928 :
929 103 : for (PatchSet.Builder b : patchSets.values()) {
930 103 : ObjectId commitId =
931 103 : b.commitId()
932 103 : .orElseThrow(
933 : () ->
934 0 : new IllegalStateException("never parsed commit ID for patch set " + b.id()));
935 103 : ChangeRevisionNote rn = rns.get(commitId);
936 103 : if (rn != null && rn.getPushCert() != null) {
937 1 : b.pushCertificate(Optional.of(rn.getPushCert()));
938 : }
939 103 : }
940 103 : }
941 :
942 : /** Parses copied {@link PatchSetApproval}. */
943 : private void parseCopiedApproval(PatchSet.Id psId, Instant ts, String line)
944 : throws ConfigInvalidException {
945 13 : ParsedPatchSetApproval parsedPatchSetApproval =
946 13 : ChangeNotesParseApprovalUtil.parseCopiedApproval(line);
947 13 : checkFooter(
948 13 : parsedPatchSetApproval.accountIdent().isPresent(),
949 : FOOTER_COPIED_LABEL,
950 13 : parsedPatchSetApproval.footerLine());
951 13 : PersonIdent accountIdent =
952 13 : RawParseUtils.parsePersonIdent(parsedPatchSetApproval.accountIdent().get());
953 :
954 13 : checkFooter(accountIdent != null, FOOTER_COPIED_LABEL, parsedPatchSetApproval.footerLine());
955 13 : Account.Id accountId = parseIdent(accountIdent);
956 :
957 13 : Account.Id realAccountId = null;
958 13 : if (parsedPatchSetApproval.realAccountIdent().isPresent()) {
959 2 : PersonIdent realIdent =
960 2 : RawParseUtils.parsePersonIdent(parsedPatchSetApproval.realAccountIdent().get());
961 2 : checkFooter(realIdent != null, FOOTER_COPIED_LABEL, parsedPatchSetApproval.footerLine());
962 2 : realAccountId = parseIdent(realIdent);
963 : }
964 :
965 : LabelVote labelVote;
966 : try {
967 13 : if (!parsedPatchSetApproval.isRemoval()) {
968 13 : labelVote = LabelVote.parseWithEquals(parsedPatchSetApproval.labelVote());
969 : } else {
970 2 : String labelName = parsedPatchSetApproval.labelVote();
971 2 : LabelType.checkNameInternal(labelName);
972 2 : labelVote = LabelVote.create(labelName, (short) 0);
973 : }
974 1 : } catch (IllegalArgumentException e) {
975 1 : ConfigInvalidException pe =
976 1 : parseException(
977 1 : "invalid %s: %s", FOOTER_COPIED_LABEL, parsedPatchSetApproval.footerLine());
978 1 : pe.initCause(e);
979 1 : throw pe;
980 13 : }
981 :
982 : PatchSetApproval.Builder psa =
983 13 : PatchSetApproval.builder()
984 13 : .key(PatchSetApproval.key(psId, accountId, LabelId.create(labelVote.label())))
985 13 : .uuid(parsedPatchSetApproval.uuid().map(PatchSetApproval::uuid))
986 13 : .value(labelVote.value())
987 13 : .granted(ts)
988 13 : .tag(parsedPatchSetApproval.tag())
989 13 : .copied(true);
990 13 : if (realAccountId != null) {
991 2 : psa.realAccountId(realAccountId);
992 : }
993 13 : approvals.putIfAbsent(psa.key(), psa);
994 13 : bufferedApprovals.add(psa);
995 13 : }
996 :
997 : private void parseApproval(
998 : PatchSet.Id psId, Account.Id accountId, Account.Id realAccountId, Instant ts, String line)
999 : throws ConfigInvalidException {
1000 68 : if (accountId == null) {
1001 1 : throw parseException("patch set %s requires an identified user as uploader", psId.get());
1002 : }
1003 : PatchSetApproval.Builder psa;
1004 68 : ParsedPatchSetApproval parsedPatchSetApproval =
1005 68 : ChangeNotesParseApprovalUtil.parseApproval(line);
1006 68 : if (line.startsWith("-")) {
1007 11 : psa = parseRemoveApproval(psId, accountId, realAccountId, ts, parsedPatchSetApproval);
1008 : } else {
1009 68 : psa = parseAddApproval(psId, accountId, realAccountId, ts, parsedPatchSetApproval);
1010 : }
1011 68 : bufferedApprovals.add(psa);
1012 68 : }
1013 :
1014 : /** Parses {@link PatchSetApproval} out of the {@link ChangeNoteFooters#FOOTER_LABEL} value. */
1015 : private PatchSetApproval.Builder parseAddApproval(
1016 : PatchSet.Id psId,
1017 : Account.Id committerId,
1018 : Account.Id realAccountId,
1019 : Instant ts,
1020 : ParsedPatchSetApproval parsedPatchSetApproval)
1021 : throws ConfigInvalidException {
1022 :
1023 68 : Account.Id approverId = parseApprover(committerId, parsedPatchSetApproval);
1024 :
1025 : LabelVote l;
1026 : try {
1027 68 : l = LabelVote.parseWithEquals(parsedPatchSetApproval.labelVote());
1028 1 : } catch (IllegalArgumentException e) {
1029 1 : ConfigInvalidException pe =
1030 1 : parseException("invalid %s: %s", FOOTER_LABEL, parsedPatchSetApproval.footerLine());
1031 1 : pe.initCause(e);
1032 1 : throw pe;
1033 68 : }
1034 :
1035 : PatchSetApproval.Builder psa =
1036 68 : PatchSetApproval.builder()
1037 68 : .key(PatchSetApproval.key(psId, approverId, LabelId.create(l.label())))
1038 68 : .uuid(parsedPatchSetApproval.uuid().map(PatchSetApproval::uuid))
1039 68 : .value(l.value())
1040 68 : .granted(ts)
1041 68 : .tag(Optional.ofNullable(tag));
1042 68 : if (!Objects.equals(realAccountId, committerId)) {
1043 4 : psa.realAccountId(realAccountId);
1044 : }
1045 68 : approvals.putIfAbsent(psa.key(), psa);
1046 68 : return psa;
1047 : }
1048 :
1049 : private PatchSetApproval.Builder parseRemoveApproval(
1050 : PatchSet.Id psId,
1051 : Account.Id committerId,
1052 : Account.Id realAccountId,
1053 : Instant ts,
1054 : ParsedPatchSetApproval parsedPatchSetApproval)
1055 : throws ConfigInvalidException {
1056 :
1057 11 : checkFooter(
1058 11 : parsedPatchSetApproval.footerLine().startsWith("-"),
1059 : FOOTER_LABEL,
1060 11 : parsedPatchSetApproval.footerLine());
1061 11 : Account.Id approverId = parseApprover(committerId, parsedPatchSetApproval);
1062 :
1063 : try {
1064 11 : LabelType.checkNameInternal(parsedPatchSetApproval.labelVote());
1065 1 : } catch (IllegalArgumentException e) {
1066 1 : ConfigInvalidException pe =
1067 1 : parseException("invalid %s: %s", FOOTER_LABEL, parsedPatchSetApproval.footerLine());
1068 1 : pe.initCause(e);
1069 1 : throw pe;
1070 11 : }
1071 :
1072 : // Store an actual 0-vote approval in the map for a removed approval, because ApprovalCopier
1073 : // needs an actual approval in order to block copying an earlier approval over a later delete.
1074 : PatchSetApproval.Builder remove =
1075 11 : PatchSetApproval.builder()
1076 11 : .key(
1077 11 : PatchSetApproval.key(
1078 11 : psId, approverId, LabelId.create(parsedPatchSetApproval.labelVote())))
1079 11 : .value(0)
1080 11 : .granted(ts);
1081 11 : if (!Objects.equals(realAccountId, committerId)) {
1082 0 : remove.realAccountId(realAccountId);
1083 : }
1084 11 : approvals.putIfAbsent(remove.key(), remove);
1085 11 : return remove;
1086 : }
1087 :
1088 : /**
1089 : * Identifies the {@link com.google.gerrit.entities.Account.Id} that issued the vote.
1090 : *
1091 : * <p>There are potentially 3 accounts involved here: 1. The account from the commit, which is the
1092 : * effective IdentifiedUser that produced the update. 2. The account in the label footer itself,
1093 : * which is used during submit to copy other users' labels to a new patch set. 3. The account in
1094 : * the Real-user footer, indicating that the whole update operation was executed by this user on
1095 : * behalf of the effective user.
1096 : */
1097 : private Account.Id parseApprover(
1098 : Account.Id committerId, ParsedPatchSetApproval parsedPatchSetApproval)
1099 : throws ConfigInvalidException {
1100 : Account.Id effectiveAccountId;
1101 68 : if (parsedPatchSetApproval.accountIdent().isPresent()) {
1102 7 : PersonIdent ident =
1103 7 : RawParseUtils.parsePersonIdent(parsedPatchSetApproval.accountIdent().get());
1104 7 : checkFooter(ident != null, FOOTER_LABEL, parsedPatchSetApproval.footerLine());
1105 7 : effectiveAccountId = parseIdent(ident);
1106 7 : } else {
1107 68 : effectiveAccountId = committerId;
1108 : }
1109 68 : return effectiveAccountId;
1110 : }
1111 :
1112 : private void parseSubmitRecords(List<String> lines) throws ConfigInvalidException {
1113 103 : SubmitRecord rec = null;
1114 :
1115 103 : for (String line : lines) {
1116 55 : int c = line.indexOf(": ");
1117 55 : if (c < 0) {
1118 55 : rec = new SubmitRecord();
1119 55 : submitRecords.add(rec);
1120 55 : int s = line.indexOf(' ');
1121 55 : String statusStr = s >= 0 ? line.substring(0, s) : line;
1122 55 : rec.status = Enums.getIfPresent(SubmitRecord.Status.class, statusStr).orNull();
1123 55 : checkFooter(rec.status != null, FOOTER_SUBMITTED_WITH, line);
1124 55 : if (s >= 0) {
1125 1 : rec.errorMessage = line.substring(s);
1126 : }
1127 55 : } else {
1128 55 : checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
1129 55 : if (line.startsWith("Rule-Name: ")) {
1130 54 : String ruleName = RULE_SPLITTER.splitToList(line).get(1);
1131 54 : rec.ruleName = ruleName;
1132 54 : continue;
1133 : }
1134 55 : SubmitRecord.Label label = new SubmitRecord.Label();
1135 55 : if (rec.labels == null) {
1136 55 : rec.labels = new ArrayList<>();
1137 : }
1138 55 : rec.labels.add(label);
1139 :
1140 55 : label.status =
1141 55 : Enums.getIfPresent(SubmitRecord.Label.Status.class, line.substring(0, c)).orNull();
1142 55 : checkFooter(label.status != null, FOOTER_SUBMITTED_WITH, line);
1143 55 : int c2 = line.indexOf(": ", c + 2);
1144 55 : if (c2 >= 0) {
1145 48 : label.label = line.substring(c + 2, c2);
1146 48 : PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(c2 + 2));
1147 48 : checkFooter(ident != null, FOOTER_SUBMITTED_WITH, line);
1148 48 : label.appliedBy = parseIdent(ident);
1149 48 : } else {
1150 13 : label.label = line.substring(c + 2);
1151 : }
1152 : }
1153 55 : }
1154 103 : }
1155 :
1156 : @Nullable
1157 : private Account.Id parseIdent(ChangeNotesCommit commit) throws ConfigInvalidException {
1158 : // Check if the author name/email is the same as the committer name/email,
1159 : // i.e. was the server ident at the time this commit was made.
1160 103 : PersonIdent a = commit.getAuthorIdent();
1161 103 : PersonIdent c = commit.getCommitterIdent();
1162 103 : if (a.getName().equals(c.getName()) && a.getEmailAddress().equals(c.getEmailAddress())) {
1163 2 : return null;
1164 : }
1165 103 : return parseIdent(a);
1166 : }
1167 :
1168 : private void parseReviewer(Instant ts, ReviewerStateInternal state, String line)
1169 : throws ConfigInvalidException {
1170 75 : PersonIdent ident = RawParseUtils.parsePersonIdent(line);
1171 75 : if (ident == null) {
1172 1 : throw invalidFooter(state.getFooterKey(), line);
1173 : }
1174 75 : Account.Id accountId = parseIdent(ident);
1175 75 : reviewerUpdates.add(ReviewerStatusUpdate.create(ts, ownerId, accountId, state));
1176 75 : if (!reviewers.containsRow(accountId)) {
1177 75 : reviewers.put(accountId, state, ts);
1178 : }
1179 75 : }
1180 :
1181 : private void parseReviewerByEmail(Instant ts, ReviewerStateInternal state, String line)
1182 : throws ConfigInvalidException {
1183 : Address adr;
1184 : try {
1185 11 : adr = Address.parse(line);
1186 0 : } catch (IllegalArgumentException e) {
1187 0 : ConfigInvalidException cie = invalidFooter(state.getByEmailFooterKey(), line);
1188 0 : cie.initCause(e);
1189 0 : throw cie;
1190 11 : }
1191 11 : if (!reviewersByEmail.containsRow(adr)) {
1192 11 : reviewersByEmail.put(adr, state, ts);
1193 : }
1194 11 : }
1195 :
1196 : private void parseIsPrivate(ChangeNotesCommit commit) throws ConfigInvalidException {
1197 103 : String raw = parseOneFooter(commit, FOOTER_PRIVATE);
1198 103 : if (raw == null) {
1199 89 : return;
1200 103 : } else if (Boolean.TRUE.toString().equalsIgnoreCase(raw)) {
1201 23 : isPrivate = true;
1202 23 : return;
1203 103 : } else if (Boolean.FALSE.toString().equalsIgnoreCase(raw)) {
1204 103 : isPrivate = false;
1205 103 : return;
1206 : }
1207 0 : throw invalidFooter(FOOTER_PRIVATE, raw);
1208 : }
1209 :
1210 : private void parseWorkInProgress(ChangeNotesCommit commit) throws ConfigInvalidException {
1211 103 : String raw = parseOneFooter(commit, FOOTER_WORK_IN_PROGRESS);
1212 103 : if (raw == null) {
1213 : // No change to WIP state in this revision.
1214 90 : previousWorkInProgressFooter = null;
1215 90 : return;
1216 103 : } else if (Boolean.TRUE.toString().equalsIgnoreCase(raw)) {
1217 : // This revision moves the change into WIP.
1218 25 : previousWorkInProgressFooter = true;
1219 25 : if (workInProgress == null) {
1220 : // Because this is the first time workInProgress is being set, we know
1221 : // that this change's current state is WIP. All the reviewer updates
1222 : // we've seen so far are pending, so take a snapshot of the reviewers
1223 : // and reviewersByEmail tables.
1224 25 : pendingReviewers =
1225 25 : ReviewerSet.fromTable(Tables.transpose(ImmutableTable.copyOf(reviewers)));
1226 25 : pendingReviewersByEmail =
1227 25 : ReviewerByEmailSet.fromTable(Tables.transpose(ImmutableTable.copyOf(reviewersByEmail)));
1228 25 : workInProgress = true;
1229 : }
1230 25 : return;
1231 103 : } else if (Boolean.FALSE.toString().equalsIgnoreCase(raw)) {
1232 103 : previousWorkInProgressFooter = false;
1233 103 : hasReviewStarted = true;
1234 103 : if (workInProgress == null) {
1235 103 : workInProgress = false;
1236 : }
1237 103 : return;
1238 : }
1239 0 : throw invalidFooter(FOOTER_WORK_IN_PROGRESS, raw);
1240 : }
1241 :
1242 : @Nullable
1243 : private Change.Id parseRevertOf(ChangeNotesCommit commit) throws ConfigInvalidException {
1244 103 : String footer = parseOneFooter(commit, FOOTER_REVERT_OF);
1245 103 : if (footer == null) {
1246 103 : return null;
1247 : }
1248 15 : Integer revertOf = Ints.tryParse(footer);
1249 15 : if (revertOf == null) {
1250 0 : throw invalidFooter(FOOTER_REVERT_OF, footer);
1251 : }
1252 15 : return Change.id(revertOf);
1253 : }
1254 :
1255 : /**
1256 : * Parses {@link ChangeNoteFooters#FOOTER_CHERRY_PICK_OF} of the commit.
1257 : *
1258 : * @param commit the commit to parse.
1259 : * @return {@link Optional} value of the parsed footer or {@code null} if the footer is missing in
1260 : * this commit.
1261 : * @throws ConfigInvalidException if the footer value could not be parsed as a valid {@link
1262 : * com.google.gerrit.entities.PatchSet.Id}.
1263 : */
1264 : @Nullable
1265 : private Optional<PatchSet.Id> parseCherryPickOf(ChangeNotesCommit commit)
1266 : throws ConfigInvalidException {
1267 103 : String footer = parseOneFooter(commit, FOOTER_CHERRY_PICK_OF);
1268 103 : if (footer == null) {
1269 : // The footer is missing, nothing to parse.
1270 103 : return null;
1271 11 : } else if (footer.equals("")) {
1272 : // Empty footer value, cherryPickOf was unset at this commit.
1273 2 : return Optional.empty();
1274 : } else {
1275 : try {
1276 11 : return Optional.of(PatchSet.Id.parse(footer));
1277 0 : } catch (IllegalArgumentException e) {
1278 0 : throw new ConfigInvalidException("\"" + footer + "\" is not a valid patchset", e);
1279 : }
1280 : }
1281 : }
1282 :
1283 : /**
1284 : * Returns the {@link Timestamp} when the commit was applied.
1285 : *
1286 : * <p>The author's date only notes when the commit was originally made. Thus, use the commiter's
1287 : * date as it accounts for the rebase, cherry-pick, commit --amend and other commands that rewrite
1288 : * the history of the branch.
1289 : *
1290 : * <p>Don't use {@link org.eclipse.jgit.revwalk.RevCommit#getCommitTime} directly because it
1291 : * returns int and would overflow.
1292 : *
1293 : * @param commit the commit to return commit time.
1294 : * @return the timestamp when the commit was applied.
1295 : */
1296 : private Instant getCommitTimestamp(ChangeNotesCommit commit) {
1297 103 : return commit.getCommitterIdent().getWhenAsInstant();
1298 : }
1299 :
1300 : private void pruneReviewers() {
1301 103 : Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Instant>> rit =
1302 103 : reviewers.cellSet().iterator();
1303 103 : while (rit.hasNext()) {
1304 75 : Table.Cell<Account.Id, ReviewerStateInternal, Instant> e = rit.next();
1305 75 : if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
1306 17 : rit.remove();
1307 : }
1308 75 : }
1309 103 : }
1310 :
1311 : private void pruneReviewersByEmail() {
1312 103 : Iterator<Table.Cell<Address, ReviewerStateInternal, Instant>> rit =
1313 103 : reviewersByEmail.cellSet().iterator();
1314 103 : while (rit.hasNext()) {
1315 11 : Table.Cell<Address, ReviewerStateInternal, Instant> e = rit.next();
1316 11 : if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
1317 8 : rit.remove();
1318 : }
1319 11 : }
1320 103 : }
1321 :
1322 : private void updatePatchSetStates() {
1323 103 : Set<PatchSet.Id> missing = new TreeSet<>(comparing(PatchSet.Id::get));
1324 103 : patchSets.keySet().stream().filter(p -> !patchSetCommitParsed(p)).forEach(p -> missing.add(p));
1325 :
1326 103 : for (Map.Entry<PatchSet.Id, PatchSetState> e : patchSetStates.entrySet()) {
1327 2 : switch (e.getValue()) {
1328 : case PUBLISHED:
1329 : default:
1330 1 : break;
1331 :
1332 : case DELETED:
1333 2 : patchSets.remove(e.getKey());
1334 : break;
1335 : }
1336 2 : }
1337 :
1338 : // Post-process other collections to remove items corresponding to deleted
1339 : // (or otherwise missing) patch sets. This is safer than trying to prevent
1340 : // insertion, as it will also filter out items racily added after the patch
1341 : // set was deleted.
1342 103 : int pruned =
1343 103 : pruneEntitiesForMissingPatchSets(allChangeMessages, ChangeMessage::getPatchSetId, missing);
1344 103 : pruned +=
1345 103 : pruneEntitiesForMissingPatchSets(
1346 103 : humanComments.values(), c -> PatchSet.id(id, c.key.patchSetId), missing);
1347 103 : pruned +=
1348 103 : pruneEntitiesForMissingPatchSets(
1349 103 : approvals.values(), psa -> psa.key().patchSetId(), missing);
1350 :
1351 103 : if (!missing.isEmpty()) {
1352 1 : logger.atWarning().log(
1353 : "ignoring %s additional entities due to missing patch sets: %s", pruned, missing);
1354 : }
1355 103 : }
1356 :
1357 : private <T> int pruneEntitiesForMissingPatchSets(
1358 : Iterable<T> ents, Function<T, PatchSet.Id> psIdFunc, Set<PatchSet.Id> missing) {
1359 103 : int pruned = 0;
1360 103 : for (Iterator<T> it = ents.iterator(); it.hasNext(); ) {
1361 103 : PatchSet.Id psId = psIdFunc.apply(it.next());
1362 103 : if (!patchSetCommitParsed(psId)) {
1363 1 : pruned++;
1364 1 : missing.add(psId);
1365 1 : it.remove();
1366 103 : } else if (deletedPatchSets.contains(psId)) {
1367 0 : it.remove(); // Not an error we need to report, don't increment pruned.
1368 : }
1369 103 : }
1370 103 : return pruned;
1371 : }
1372 :
1373 : private void checkMandatoryFooters() throws ConfigInvalidException {
1374 103 : List<FooterKey> missing = new ArrayList<>();
1375 103 : if (branch == null) {
1376 0 : missing.add(FOOTER_BRANCH);
1377 : }
1378 103 : if (changeId == null) {
1379 0 : missing.add(FOOTER_CHANGE_ID);
1380 : }
1381 103 : if (originalSubject == null || subject == null) {
1382 0 : missing.add(FOOTER_SUBJECT);
1383 : }
1384 103 : if (!missing.isEmpty()) {
1385 0 : throw parseException(
1386 : "%s",
1387 0 : "Missing footers: " + missing.stream().map(FooterKey::getName).collect(joining(", ")));
1388 : }
1389 103 : }
1390 :
1391 : private ConfigInvalidException expectedOneFooter(FooterKey footer, List<String> actual) {
1392 1 : return parseException("missing or multiple %s: %s", footer.getName(), actual);
1393 : }
1394 :
1395 : private ConfigInvalidException invalidFooter(FooterKey footer, String actual) {
1396 1 : return parseException("invalid %s: %s", footer.getName(), actual);
1397 : }
1398 :
1399 : private void checkFooter(boolean expr, FooterKey footer, String actual)
1400 : throws ConfigInvalidException {
1401 57 : if (!expr) {
1402 1 : throw invalidFooter(footer, actual);
1403 : }
1404 57 : }
1405 :
1406 : private void checkPatchSetCommitNotParsed(PatchSet.Id psId, FooterKey footer)
1407 : throws ConfigInvalidException {
1408 103 : if (patchSetCommitParsed(psId)) {
1409 0 : throw parseException(
1410 : "%s field found for patch set %s before patch set was originally defined",
1411 0 : footer.getName(), psId.get());
1412 : }
1413 103 : }
1414 :
1415 : private boolean patchSetCommitParsed(PatchSet.Id psId) {
1416 103 : PatchSet.Builder pending = patchSets.get(psId);
1417 103 : return pending != null && pending.commitId().isPresent();
1418 : }
1419 :
1420 : @FormatMethod
1421 : private ConfigInvalidException parseException(String fmt, Object... args) {
1422 1 : return ChangeNotes.parseException(id, fmt, args);
1423 : }
1424 :
1425 : private Account.Id parseIdent(PersonIdent ident) throws ConfigInvalidException {
1426 103 : return NoteDbUtil.parseIdent(ident, gerritServerId, externalIdCache)
1427 103 : .orElseThrow(
1428 1 : () -> parseException("cannot retrieve account id: %s", ident.getEmailAddress()));
1429 : }
1430 :
1431 : protected boolean countTowardsMaxUpdatesLimit(
1432 : ChangeNotesCommit commit, boolean hasChangeMessage) {
1433 103 : return !commit.isAttentionSetCommitOnly(hasChangeMessage);
1434 : }
1435 : }
|