com.google.gerrit.server.notedb.ChangeNotesParser Maven / Gradle / Ivy
// Copyright (C) 2014 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.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
import com.google.common.base.Enums;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.LabelVote;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.util.RawParseUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
class ChangeNotesParser {
// Sentinel RevId indicating a mutable field on a patch set was parsed, but
// the parser does not yet know its commit SHA-1.
private static final RevId PARTIAL_PATCH_SET =
new RevId("INVALID PARTIAL PATCH SET");
// Private final members initialized in the constructor.
private final ChangeNoteUtil noteUtil;
private final NoteDbMetrics metrics;
private final Change.Id id;
private final ObjectId tip;
private final ChangeNotesRevWalk walk;
// Private final but mutable members initialized in the constructor and filled
// in during the parsing process.
private final Table reviewers;
private final List allPastReviewers;
private final List reviewerUpdates;
private final List submitRecords;
private final Multimap comments;
private final TreeMap patchSets;
private final Set deletedPatchSets;
private final Map patchSetStates;
private final Map, Optional>> approvals;
private final List allChangeMessages;
private final Multimap changeMessagesByPatchSet;
// Non-final private members filled in during the parsing process.
private String branch;
private Change.Status status;
private String topic;
private Set hashtags;
private Timestamp createdOn;
private Timestamp lastUpdatedOn;
private Account.Id ownerId;
private String changeId;
private String subject;
private String originalSubject;
private String submissionId;
private String tag;
private PatchSet.Id currentPatchSetId;
private RevisionNoteMap revisionNoteMap;
ChangeNotesParser(Change.Id changeId, ObjectId tip, ChangeNotesRevWalk walk,
ChangeNoteUtil noteUtil, NoteDbMetrics metrics) {
this.id = changeId;
this.tip = tip;
this.walk = walk;
this.noteUtil = noteUtil;
this.metrics = metrics;
approvals = new HashMap<>();
reviewers = HashBasedTable.create();
allPastReviewers = new ArrayList<>();
reviewerUpdates = new ArrayList<>();
submitRecords = Lists.newArrayListWithExpectedSize(1);
allChangeMessages = new ArrayList<>();
changeMessagesByPatchSet = LinkedListMultimap.create();
comments = ArrayListMultimap.create();
patchSets = Maps.newTreeMap(ReviewDbUtil.intKeyOrdering());
deletedPatchSets = new HashSet<>();
patchSetStates = new HashMap<>();
}
ChangeNotesState parseAll()
throws ConfigInvalidException, IOException {
// Don't include initial parse in timer, as this might do more I/O to page
// in the block containing most commits. Later reads are not guaranteed to
// avoid I/O, but often should.
walk.reset();
walk.markStart(walk.parseCommit(tip));
try (Timer1.Context timer = metrics.parseLatency.start(CHANGES)) {
ChangeNotesCommit commit;
while ((commit = walk.next()) != null) {
parse(commit);
}
parseNotes();
allPastReviewers.addAll(reviewers.rowKeySet());
pruneReviewers();
updatePatchSetStates();
checkMandatoryFooters();
}
return buildState();
}
RevisionNoteMap getRevisionNoteMap() {
return revisionNoteMap;
}
private ChangeNotesState buildState() {
return ChangeNotesState.create(
id,
new Change.Key(changeId),
createdOn,
lastUpdatedOn,
ownerId,
branch,
currentPatchSetId,
subject,
topic,
originalSubject,
submissionId,
status,
hashtags,
patchSets,
buildApprovals(),
ReviewerSet.fromTable(Tables.transpose(reviewers)),
allPastReviewers,
buildReviewerUpdates(),
submitRecords,
buildAllMessages(),
buildMessagesByPatchSet(),
comments);
}
private Multimap buildApprovals() {
Multimap result =
ArrayListMultimap.create(approvals.keySet().size(), 3);
for (Table, ?, Optional> curr : approvals.values()) {
for (Optional psa : curr.values()) {
if (psa.isPresent()) {
result.put(psa.get().getPatchSetId(), psa.get());
}
}
}
for (Collection v : result.asMap().values()) {
Collections.sort((List) v, ChangeNotes.PSA_BY_TIME);
}
return result;
}
private List buildReviewerUpdates() {
List result = new ArrayList<>();
HashMap lastState = new HashMap<>();
for (ReviewerStatusUpdate u : Lists.reverse(reviewerUpdates)) {
if (!Objects.equals(ownerId, u.reviewer()) &&
lastState.get(u.reviewer()) != u.state()) {
result.add(u);
lastState.put(u.reviewer(), u.state());
}
}
return result;
}
private List buildAllMessages() {
return Lists.reverse(allChangeMessages);
}
private Multimap buildMessagesByPatchSet() {
for (Collection v :
changeMessagesByPatchSet.asMap().values()) {
Collections.reverse((List) v);
}
return changeMessagesByPatchSet;
}
private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
Timestamp ts =
new Timestamp(commit.getCommitterIdent().getWhen().getTime());
createdOn = ts;
parseTag(commit);
if (branch == null) {
branch = parseBranch(commit);
}
if (status == null) {
status = parseStatus(commit);
}
PatchSet.Id psId = parsePatchSetId(commit);
if (currentPatchSetId == null || psId.get() > currentPatchSetId.get()) {
currentPatchSetId = psId;
}
PatchSetState psState = parsePatchSetState(commit);
if (psState != null) {
if (!patchSetStates.containsKey(psId)) {
patchSetStates.put(psId, psState);
}
if (psState == PatchSetState.DELETED) {
deletedPatchSets.add(psId);
}
}
Account.Id accountId = parseIdent(commit);
if (accountId != null) {
ownerId = accountId;
}
if (changeId == null) {
changeId = parseChangeId(commit);
}
String currSubject = parseSubject(commit);
if (currSubject != null) {
if (subject == null) {
subject = currSubject;
}
originalSubject = currSubject;
}
parseChangeMessage(psId, accountId, commit, ts);
if (topic == null) {
topic = parseTopic(commit);
}
parseHashtags(commit);
if (submissionId == null) {
submissionId = parseSubmissionId(commit);
}
ObjectId currRev = parseRevision(commit);
if (currRev != null) {
parsePatchSet(psId, currRev, accountId, ts);
}
parseGroups(psId, commit);
if (submitRecords.isEmpty()) {
// Only parse the most recent set of submit records; any older ones are
// still there, but not currently used.
parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
}
for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
parseApproval(psId, accountId, ts, line);
}
for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
for (String line : commit.getFooterLineValues(state.getFooterKey())) {
parseReviewer(ts, state, line);
}
// Don't update timestamp when a reviewer was added, matching RevewDb
// behavior.
}
if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
lastUpdatedOn = ts;
}
}
private String parseSubmissionId(ChangeNotesCommit commit)
throws ConfigInvalidException {
return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
}
private String parseBranch(ChangeNotesCommit commit)
throws ConfigInvalidException {
String branch = parseOneFooter(commit, FOOTER_BRANCH);
return branch != null ? RefNames.fullName(branch) : null;
}
private String parseChangeId(ChangeNotesCommit commit)
throws ConfigInvalidException {
return parseOneFooter(commit, FOOTER_CHANGE_ID);
}
private String parseSubject(ChangeNotesCommit commit)
throws ConfigInvalidException {
return parseOneFooter(commit, FOOTER_SUBJECT);
}
private String parseTopic(ChangeNotesCommit commit)
throws ConfigInvalidException {
return parseOneFooter(commit, FOOTER_TOPIC);
}
private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
throws ConfigInvalidException {
List footerLines = commit.getFooterLineValues(footerKey);
if (footerLines.isEmpty()) {
return null;
} else if (footerLines.size() > 1) {
throw expectedOneFooter(footerKey, footerLines);
}
return footerLines.get(0);
}
private String parseExactlyOneFooter(ChangeNotesCommit commit,
FooterKey footerKey) throws ConfigInvalidException {
String line = parseOneFooter(commit, footerKey);
if (line == null) {
throw expectedOneFooter(footerKey, Collections. emptyList());
}
return line;
}
private ObjectId parseRevision(ChangeNotesCommit commit)
throws ConfigInvalidException {
String sha = parseOneFooter(commit, FOOTER_COMMIT);
if (sha == null) {
return null;
}
try {
return ObjectId.fromString(sha);
} catch (InvalidObjectIdException e) {
ConfigInvalidException cie = invalidFooter(FOOTER_COMMIT, sha);
cie.initCause(e);
throw cie;
}
}
private void parsePatchSet(PatchSet.Id psId, ObjectId rev,
Account.Id accountId, Timestamp ts) throws ConfigInvalidException {
if (accountId == null) {
throw parseException(
"patch set %s requires an identified user as uploader", psId.get());
}
PatchSet ps = patchSets.get(psId);
if (ps == null) {
ps = new PatchSet(psId);
patchSets.put(psId, ps);
} else if (!ps.getRevision().equals(PARTIAL_PATCH_SET)) {
if (deletedPatchSets.contains(psId)) {
// Do not update PS details as PS was deleted and this meta data is of
// no relevance
return;
}
throw new ConfigInvalidException(
String.format(
"Multiple revisions parsed for patch set %s: %s and %s",
psId.get(), patchSets.get(psId).getRevision(), rev.name()));
}
ps.setRevision(new RevId(rev.name()));
ps.setUploader(accountId);
ps.setCreatedOn(ts);
}
private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
throws ConfigInvalidException {
String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
if (groupsStr == null) {
return;
}
PatchSet ps = patchSets.get(psId);
if (ps == null) {
ps = new PatchSet(psId);
ps.setRevision(PARTIAL_PATCH_SET);
patchSets.put(psId, ps);
} else if (!ps.getGroups().isEmpty()) {
return;
}
ps.setGroups(PatchSet.splitGroups(groupsStr));
}
private void parseHashtags(ChangeNotesCommit commit)
throws ConfigInvalidException {
// Commits are parsed in reverse order and only the last set of hashtags
// should be used.
if (hashtags != null) {
return;
}
List hashtagsLines = commit.getFooterLineValues(FOOTER_HASHTAGS);
if (hashtagsLines.isEmpty()) {
return;
} else if (hashtagsLines.size() > 1) {
throw expectedOneFooter(FOOTER_HASHTAGS, hashtagsLines);
} else if (hashtagsLines.get(0).isEmpty()) {
hashtags = ImmutableSet.of();
} else {
hashtags = Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
}
}
private void parseTag(ChangeNotesCommit commit)
throws ConfigInvalidException {
tag = null;
List tagLines = commit.getFooterLineValues(FOOTER_TAG);
if (tagLines.isEmpty()) {
return;
} else if (tagLines.size() == 1) {
tag = tagLines.get(0);
} else {
throw expectedOneFooter(FOOTER_TAG, tagLines);
}
}
private Change.Status parseStatus(ChangeNotesCommit commit)
throws ConfigInvalidException {
List statusLines = commit.getFooterLineValues(FOOTER_STATUS);
if (statusLines.isEmpty()) {
return null;
} else if (statusLines.size() > 1) {
throw expectedOneFooter(FOOTER_STATUS, statusLines);
}
Optional status = Enums.getIfPresent(
Change.Status.class, statusLines.get(0).toUpperCase());
if (!status.isPresent()) {
throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
}
return status.get();
}
private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit)
throws ConfigInvalidException {
String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
int s = psIdLine.indexOf(' ');
String psIdStr = s < 0 ? psIdLine : psIdLine.substring(0, s);
Integer psId = Ints.tryParse(psIdStr);
if (psId == null) {
throw invalidFooter(FOOTER_PATCH_SET, psIdStr);
}
return new PatchSet.Id(id, psId);
}
private PatchSetState parsePatchSetState(ChangeNotesCommit commit)
throws ConfigInvalidException {
String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
int s = psIdLine.indexOf(' ');
if (s < 0) {
return null;
}
String withParens = psIdLine.substring(s + 1);
if (withParens.startsWith("(") && withParens.endsWith(")")) {
Optional state = Enums.getIfPresent(PatchSetState.class,
withParens.substring(1, withParens.length() - 1).toUpperCase());
if (state.isPresent()) {
return state.get();
}
}
throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
}
private void parseChangeMessage(PatchSet.Id psId,
Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
byte[] raw = commit.getRawBuffer();
int size = raw.length;
Charset enc = RawParseUtils.parseEncoding(raw);
int subjectStart = RawParseUtils.commitMessage(raw, 0);
if (subjectStart < 0 || subjectStart >= size) {
return;
}
int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
if (subjectEnd == size) {
return;
}
int changeMessageStart;
if (raw[subjectEnd] == '\n') {
changeMessageStart = subjectEnd + 2; //\n\n ends paragraph
} else if (raw[subjectEnd] == '\r') {
changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
} else {
return;
}
int ptr = size - 1;
int changeMessageEnd = -1;
while (ptr > changeMessageStart) {
ptr = RawParseUtils.prevLF(raw, ptr, '\r');
if (ptr == -1) {
break;
}
if (raw[ptr] == '\n') {
changeMessageEnd = ptr - 1;
break;
} else if (raw[ptr] == '\r') {
changeMessageEnd = ptr - 3;
break;
}
}
if (ptr <= changeMessageStart) {
return;
}
String changeMsgString = RawParseUtils.decode(enc, raw,
changeMessageStart, changeMessageEnd + 1);
ChangeMessage changeMessage = new ChangeMessage(
new ChangeMessage.Key(psId.getParentKey(), commit.name()),
accountId,
ts,
psId);
changeMessage.setMessage(changeMsgString);
changeMessage.setTag(tag);
changeMessagesByPatchSet.put(psId, changeMessage);
allChangeMessages.add(changeMessage);
}
private void parseNotes()
throws IOException, ConfigInvalidException {
ObjectReader reader = walk.getObjectReader();
ChangeNotesCommit tipCommit = walk.parseCommit(tip);
revisionNoteMap = RevisionNoteMap.parse(
noteUtil, id, reader, NoteMap.read(reader, tipCommit), false);
Map rns = revisionNoteMap.revisionNotes;
for (Map.Entry e : rns.entrySet()) {
for (PatchLineComment plc : e.getValue().comments) {
comments.put(e.getKey(), plc);
}
}
for (PatchSet ps : patchSets.values()) {
RevisionNote rn = rns.get(ps.getRevision());
if (rn != null && rn.pushCert != null) {
ps.setPushCertificate(rn.pushCert);
}
}
}
private void parseApproval(PatchSet.Id psId, Account.Id accountId,
Timestamp ts, String line) throws ConfigInvalidException {
if (accountId == null) {
throw parseException(
"patch set %s requires an identified user as uploader", psId.get());
}
if (line.startsWith("-")) {
parseRemoveApproval(psId, accountId, line);
} else {
parseAddApproval(psId, accountId, ts, line);
}
}
private void parseAddApproval(PatchSet.Id psId, Account.Id committerId,
Timestamp ts, String line) throws ConfigInvalidException {
Account.Id accountId;
String labelVoteStr;
int s = line.indexOf(' ');
if (s > 0) {
labelVoteStr = line.substring(0, s);
PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
checkFooter(ident != null, FOOTER_LABEL, line);
accountId = noteUtil.parseIdent(ident, id);
} else {
labelVoteStr = line;
accountId = committerId;
}
LabelVote l;
try {
l = LabelVote.parseWithEquals(labelVoteStr);
} catch (IllegalArgumentException e) {
ConfigInvalidException pe =
parseException("invalid %s: %s", FOOTER_LABEL, line);
pe.initCause(e);
throw pe;
}
Entry label = Maps.immutableEntry(l.label(), tag);
Table, Optional> curr =
getApprovalsTableIfNoVotePresent(psId, accountId, label);
if (curr != null) {
PatchSetApproval psa = new PatchSetApproval(
new PatchSetApproval.Key(
psId,
accountId,
new LabelId(l.label())),
l.value(),
ts);
psa.setTag(tag);
curr.put(accountId, label, Optional.of(psa));
}
}
private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
String line) throws ConfigInvalidException {
Account.Id accountId;
Entry label;
int s = line.indexOf(' ');
if (s > 0) {
label = Maps.immutableEntry(line.substring(1, s), tag);
PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
checkFooter(ident != null, FOOTER_LABEL, line);
accountId = noteUtil.parseIdent(ident, id);
} else {
label = Maps.immutableEntry(line.substring(1), tag);
accountId = committerId;
}
try {
LabelType.checkNameInternal(label.getKey());
} catch (IllegalArgumentException e) {
ConfigInvalidException pe =
parseException("invalid %s: %s", FOOTER_LABEL, line);
pe.initCause(e);
throw pe;
}
Table, Optional> curr =
getApprovalsTableIfNoVotePresent(psId, accountId, label);
if (curr != null) {
curr.put(accountId, label, Optional. absent());
}
}
private Table, Optional>
getApprovalsTableIfNoVotePresent(PatchSet.Id psId, Account.Id accountId,
Entry label) {
Table, Optional> curr =
approvals.get(psId);
if (curr != null) {
if (curr.contains(accountId, label)) {
return null;
}
} else {
curr = Tables.newCustomTable(
Maps., Optional>>
newHashMapWithExpectedSize(2),
new Supplier
© 2015 - 2025 Weber Informatics LLC | Privacy Policy